Add topology legend and node visibility filters.

Show color legend and allow filtering graph nodes by all/new/missing states after scan diff analysis.

Made-with: Cursor
This commit is contained in:
Andrey Lutsenko
2026-04-09 22:13:36 +10:00
parent 6f6b822f16
commit 22c07c7fb1
3 changed files with 42 additions and 4 deletions
+1
View File
@@ -63,6 +63,7 @@ http://localhost:8080/
- сравнить два scan прямо на странице (`from`/`to`) через `/api/scans/diff`. - сравнить два scan прямо на странице (`from`/`to`) через `/api/scans/diff`.
- выбрать scan из выпадающего списка последних 20; - выбрать scan из выпадающего списка последних 20;
- подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff. - подсветить на графе новые (`[+]`, зеленый) и пропавшие (`[-]`, красный) хосты по diff.
- использовать легенду цветов и фильтр узлов: все / только новые / только пропавшие.
По умолчанию используется `STORE_BACKEND=memory`. По умолчанию используется `STORE_BACKEND=memory`.
SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`). SNMP-проверка по умолчанию включена (`SNMP_ENABLED=true`, community `public`).
+1
View File
@@ -53,6 +53,7 @@
- [x] подготовлены конфиги для запуска как сервис на Linux и за IIS на Windows. - [x] подготовлены конфиги для запуска как сервис на Linux и за IIS на Windows.
- [x] UI позволяет выбрать последний scan и выполнить diff двух сканов. - [x] UI позволяет выбрать последний scan и выполнить diff двух сканов.
- [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе. - [x] UI позволяет выбирать scan из списка и подсвечивать изменения на графе.
- [x] UI содержит легенду и фильтр отображения узлов по diff.
## Минимальные API ## Минимальные API
+40 -4
View File
@@ -31,6 +31,18 @@
<button id="diffBtn">Сравнить</button> <button id="diffBtn">Сравнить</button>
</div> </div>
<div id="status">Ожидание данных...</div> <div id="status">Ожидание данных...</div>
<div style="margin: 6px 0 10px 0; font-size: 13px;">
Легенда:
<span style="display:inline-block;margin-left:8px;color:#2563eb;">● синий - обычный узел</span>
<span style="display:inline-block;margin-left:8px;color:#16a34a;">● зеленый - новый</span>
<span style="display:inline-block;margin-left:8px;color:#dc2626;">● красный - пропавший</span>
<span style="display:inline-block;margin-left:12px;">Фильтр:</span>
<select id="nodeFilter">
<option value="all">все</option>
<option value="new">только новые</option>
<option value="missing">только пропавшие</option>
</select>
</div>
<pre id="diffBox" style="background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px;white-space:pre-wrap;"></pre> <pre id="diffBox" style="background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:10px;white-space:pre-wrap;"></pre>
<svg id="canvas" viewBox="0 0 1200 620"></svg> <svg id="canvas" viewBox="0 0 1200 620"></svg>
@@ -45,10 +57,13 @@
const latestBtn = document.getElementById("latestBtn"); const latestBtn = document.getElementById("latestBtn");
const diffBtn = document.getElementById("diffBtn"); const diffBtn = document.getElementById("diffBtn");
const fitBtn = document.getElementById("fitBtn"); const fitBtn = document.getElementById("fitBtn");
const nodeFilter = document.getElementById("nodeFilter");
const statusEl = document.getElementById("status"); const statusEl = document.getElementById("status");
const diffBox = document.getElementById("diffBox"); const diffBox = document.getElementById("diffBox");
const svg = document.getElementById("canvas"); const svg = document.getElementById("canvas");
const NS = "http://www.w3.org/2000/svg"; const NS = "http://www.w3.org/2000/svg";
let lastTopology = null;
let lastHighlight = 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; }
@@ -59,8 +74,17 @@
function drawTopology(data, highlight = null) { function drawTopology(data, highlight = null) {
clearSVG(); clearSVG();
const nodes = data.nodes || []; let nodes = data.nodes || [];
const edges = data.edges || []; const edges = data.edges || [];
const filter = nodeFilter.value || "all";
if (highlight && (filter === "new" || filter === "missing")) {
nodes = nodes.filter(n => {
const isNew = highlight.newHosts && highlight.newHosts.has(n.ip);
const isMissing = highlight.missingHosts && highlight.missingHosts.has(n.ip);
return filter === "new" ? isNew : isMissing;
});
}
const visibleIPs = new Set(nodes.map(n => n.ip));
const byIP = new Map(); const byIP = new Map();
nodes.forEach(n => byIP.set(n.ip, n)); nodes.forEach(n => byIP.set(n.ip, n));
@@ -79,6 +103,8 @@
edges.forEach(e => { edges.forEach(e => {
const s = pos.get(e.source_ip); const s = pos.get(e.source_ip);
if (!s) return; if (!s) return;
if (!visibleIPs.has(e.source_ip)) return;
if (e.target_ip && !visibleIPs.has(e.target_ip)) return;
const t = pos.get(e.target_ip) || { x: s.x + 30, y: s.y + 30 }; const t = pos.get(e.target_ip) || { x: s.x + 30, y: s.y + 30 };
const line = document.createElementNS(NS, "line"); const line = document.createElementNS(NS, "line");
@@ -165,7 +191,9 @@
return; return;
} }
const data = await res.json(); const data = await res.json();
drawTopology(data); lastTopology = data;
lastHighlight = null;
drawTopology(data, null);
setStatus(`Готово. Узлов: ${data.nodes?.length || 0}, связей: ${data.edges?.length || 0}`); setStatus(`Готово. Узлов: ${data.nodes?.length || 0}, связей: ${data.edges?.length || 0}`);
} catch (e) { } catch (e) {
setStatus(`Ошибка загрузки: ${e.message}`); setStatus(`Ошибка загрузки: ${e.message}`);
@@ -216,10 +244,13 @@
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) {
const topo = await topoRes.json(); const topo = await topoRes.json();
drawTopology(topo, { const highlight = {
newHosts: new Set(d.new_hosts || []), newHosts: new Set(d.new_hosts || []),
missingHosts: new Set(d.missing_hosts || []), missingHosts: new Set(d.missing_hosts || []),
}); };
lastTopology = topo;
lastHighlight = highlight;
drawTopology(topo, highlight);
} }
setStatus( setStatus(
`Diff готов: +hosts ${d.new_hosts?.length || 0}, -hosts ${d.missing_hosts?.length || 0}, +links ${d.new_links?.length || 0}, -links ${d.missing_links?.length || 0}` `Diff готов: +hosts ${d.new_hosts?.length || 0}, -hosts ${d.missing_hosts?.length || 0}, +links ${d.new_links?.length || 0}, -links ${d.missing_links?.length || 0}`
@@ -244,6 +275,11 @@
fitBtn.addEventListener("click", () => { fitBtn.addEventListener("click", () => {
svg.setAttribute("viewBox", "0 0 1200 620"); svg.setAttribute("viewBox", "0 0 1200 620");
}); });
nodeFilter.addEventListener("change", () => {
if (lastTopology) {
drawTopology(lastTopology, lastHighlight);
}
});
setDiff("Diff будет показан здесь..."); setDiff("Diff будет показан здесь...");
loadScanOptions(); loadScanOptions();
</script> </script>