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:
@@ -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`).
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user