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