Add JSON export actions for topology and diff.

Provide UI buttons to export latest topology and diff payloads into downloadable JSON files.

Made-with: Cursor
This commit is contained in:
Andrey Lutsenko
2026-04-09 22:15:44 +10:00
parent 22c07c7fb1
commit 396d139482
3 changed files with 38 additions and 0 deletions
+1
View File
@@ -64,6 +64,7 @@ http://localhost:8080/
- выбрать scan из выпадающего списка последних 20; - выбрать scan из выпадающего списка последних 20;
- подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff. - подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff.
- использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие. - использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие.
- экспортировать topology и diff в JSON-файлы кнопками из UI.
По умолчанию используется `STORE_BACKEND=memory`. По умолчанию используется `STORE_BACKEND=memory`.
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`). SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
+1
View File
@@ -54,6 +54,7 @@
- [x] UI позволяет выбрать последний scan и выполнить diff двух сканов. - [x] UI позволяет выбрать последний scan и выполнить diff двух сканов.
- [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе. - [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе.
- [x] UI содержит легенду и фильтр отображения узлов по diff. - [x] UI содержит легенду и фильтр отображения узлов по diff.
- [x] UI поддерживает экспорт topology/diff в JSON.
## Минимальные API ## Минимальные API
+36
View File
@@ -21,6 +21,7 @@
<select id="scanSelect" style="min-width: 360px;"></select> <select id="scanSelect" style="min-width: 360px;"></select>
<button id="loadBtn">Загрузить</button> <button id="loadBtn">Загрузить</button>
<button id="latestBtn">Последний scan</button> <button id="latestBtn">Последний scan</button>
<button id="exportTopologyBtn">Экспорт topology JSON</button>
<button id="fitBtn">По центру</button> <button id="fitBtn">По центру</button>
</div> </div>
<div class="row"> <div class="row">
@@ -29,6 +30,7 @@
<select id="fromScanSelect" style="min-width: 240px;"></select> <select id="fromScanSelect" style="min-width: 240px;"></select>
<select id="toScanSelect" style="min-width: 240px;"></select> <select id="toScanSelect" style="min-width: 240px;"></select>
<button id="diffBtn">Сравнить</button> <button id="diffBtn">Сравнить</button>
<button id="exportDiffBtn">Экспорт diff JSON</button>
</div> </div>
<div id="status">Ожидание данных...</div> <div id="status">Ожидание данных...</div>
<div style="margin: 6px 0 10px 0; font-size: 13px;"> <div style="margin: 6px 0 10px 0; font-size: 13px;">
@@ -56,6 +58,8 @@
const loadBtn = document.getElementById("loadBtn"); const loadBtn = document.getElementById("loadBtn");
const latestBtn = document.getElementById("latestBtn"); const latestBtn = document.getElementById("latestBtn");
const diffBtn = document.getElementById("diffBtn"); const diffBtn = document.getElementById("diffBtn");
const exportTopologyBtn = document.getElementById("exportTopologyBtn");
const exportDiffBtn = document.getElementById("exportDiffBtn");
const fitBtn = document.getElementById("fitBtn"); const fitBtn = document.getElementById("fitBtn");
const nodeFilter = document.getElementById("nodeFilter"); const nodeFilter = document.getElementById("nodeFilter");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
@@ -64,9 +68,21 @@
const NS = "http://www.w3.org/2000/svg"; const NS = "http://www.w3.org/2000/svg";
let lastTopology = null; let lastTopology = null;
let lastHighlight = null; let lastHighlight = null;
let lastDiff = null;
function setStatus(text) { statusEl.textContent = text; } function setStatus(text) { statusEl.textContent = text; }
function setDiff(text) { diffBox.textContent = text; } function setDiff(text) { diffBox.textContent = text; }
function downloadJSON(filename, data) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function clearSVG() { function clearSVG() {
while (svg.firstChild) svg.removeChild(svg.firstChild); while (svg.firstChild) svg.removeChild(svg.firstChild);
@@ -240,6 +256,7 @@
return; return;
} }
const d = await res.json(); const d = await res.json();
lastDiff = d;
setDiff(JSON.stringify(d, null, 2)); setDiff(JSON.stringify(d, null, 2));
const topoRes = await fetch(`/api/scans/${encodeURIComponent(toId)}/topology?limit=5000`); const topoRes = await fetch(`/api/scans/${encodeURIComponent(toId)}/topology?limit=5000`);
if (topoRes.ok) { if (topoRes.ok) {
@@ -272,6 +289,25 @@
}); });
latestBtn.addEventListener("click", loadLatestScan); latestBtn.addEventListener("click", loadLatestScan);
diffBtn.addEventListener("click", loadDiff); diffBtn.addEventListener("click", loadDiff);
exportTopologyBtn.addEventListener("click", () => {
const scanId = scanIdInput.value.trim() || "unknown";
if (!lastTopology) {
setStatus("Сначала загрузите topology");
return;
}
downloadJSON(`topology_${scanId}.json`, lastTopology);
setStatus("Topology JSON экспортирован");
});
exportDiffBtn.addEventListener("click", () => {
const fromId = fromScanIdInput.value.trim() || "from";
const toId = toScanIdInput.value.trim() || "to";
if (!lastDiff) {
setStatus("Сначала выполните diff");
return;
}
downloadJSON(`diff_${fromId}_to_${toId}.json`, lastDiff);
setStatus("Diff JSON экспортирован");
});
fitBtn.addEventListener("click", () => { fitBtn.addEventListener("click", () => {
svg.setAttribute("viewBox", "0 0 1200 620"); svg.setAttribute("viewBox", "0 0 1200 620");
}); });