feat(network-map): add critical and warning node displays with risk assessment for improved network monitoring
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m28s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m28s
This commit is contained in:
@@ -287,6 +287,7 @@ export default function NetworkMapUnifi({
|
|||||||
to,
|
to,
|
||||||
p1,
|
p1,
|
||||||
p2,
|
p2,
|
||||||
|
pingMs: typeof ms === 'number' ? ms : null,
|
||||||
pingLabel,
|
pingLabel,
|
||||||
speedLabel,
|
speedLabel,
|
||||||
compactEdgeLabel,
|
compactEdgeLabel,
|
||||||
@@ -477,16 +478,114 @@ export default function NetworkMapUnifi({
|
|||||||
};
|
};
|
||||||
}, [computeCurvePoints, size.w, size.h]);
|
}, [computeCurvePoints, size.w, size.h]);
|
||||||
|
|
||||||
|
const problematicNodes = useMemo(() => {
|
||||||
|
const byIp = new Map();
|
||||||
|
servers.forEach((s) => {
|
||||||
|
byIp.set(String(s.ip), {
|
||||||
|
ip: String(s.ip),
|
||||||
|
dns: s.dns || '',
|
||||||
|
type: String(s.type || '').toLowerCase(),
|
||||||
|
linkCount: 0,
|
||||||
|
staleCount: 0,
|
||||||
|
maxPingMs: null,
|
||||||
|
minSpeedMbps: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
(connections || []).forEach((c) => {
|
||||||
|
const from = byIp.get(String(c.from));
|
||||||
|
const to = byIp.get(String(c.to));
|
||||||
|
if (from) from.linkCount += 1;
|
||||||
|
if (to) to.linkCount += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
routedEdges.forEach((e) => {
|
||||||
|
[e.from, e.to].forEach((nodeIp) => {
|
||||||
|
const node = byIp.get(String(nodeIp));
|
||||||
|
if (!node) return;
|
||||||
|
if (e.stale) node.staleCount += 1;
|
||||||
|
if (typeof e.pingMs === 'number') {
|
||||||
|
node.maxPingMs = node.maxPingMs == null ? e.pingMs : Math.max(node.maxPingMs, e.pingMs);
|
||||||
|
}
|
||||||
|
const down = e.speed?.tcpDownloadBps != null ? e.speed.tcpDownloadBps / 1_000_000 : null;
|
||||||
|
const up = e.speed?.tcpUploadBps != null ? e.speed.tcpUploadBps / 1_000_000 : null;
|
||||||
|
const edgeMin = down != null && up != null ? Math.min(down, up) : (down ?? up);
|
||||||
|
if (edgeMin != null && Number.isFinite(edgeMin)) {
|
||||||
|
node.minSpeedMbps = node.minSpeedMbps == null ? edgeMin : Math.min(node.minSpeedMbps, edgeMin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(byIp.values())
|
||||||
|
.map((n) => {
|
||||||
|
const issues = [];
|
||||||
|
let riskScore = 0;
|
||||||
|
|
||||||
|
if (n.maxPingMs != null) {
|
||||||
|
if (n.maxPingMs >= 120) {
|
||||||
|
issues.push('Очень высокий ping');
|
||||||
|
riskScore += 3;
|
||||||
|
} else if (n.maxPingMs >= 80) {
|
||||||
|
issues.push('Повышенный ping');
|
||||||
|
riskScore += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.minSpeedMbps != null) {
|
||||||
|
if (n.minSpeedMbps < 40) {
|
||||||
|
issues.push('Низкая скорость');
|
||||||
|
riskScore += 3;
|
||||||
|
} else if (n.minSpeedMbps < 80) {
|
||||||
|
issues.push('Скорость ниже нормы');
|
||||||
|
riskScore += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.staleCount > 0) {
|
||||||
|
issues.push('Есть устаревшие метрики');
|
||||||
|
riskScore += n.staleCount >= 2 ? 2 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.linkCount >= 7) {
|
||||||
|
issues.push('Высокая связанность');
|
||||||
|
riskScore += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
issues,
|
||||||
|
riskScore,
|
||||||
|
nodeLabel: n.dns ? String(n.dns).split('.')[0] : n.ip,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((n) => n.issues.length > 0)
|
||||||
|
.sort((a, b) =>
|
||||||
|
b.riskScore - a.riskScore ||
|
||||||
|
(b.maxPingMs ?? -1) - (a.maxPingMs ?? -1) ||
|
||||||
|
(a.minSpeedMbps ?? Infinity) - (b.minSpeedMbps ?? Infinity)
|
||||||
|
);
|
||||||
|
}, [servers, connections, routedEdges]);
|
||||||
|
|
||||||
|
const criticalNodes = useMemo(
|
||||||
|
() => problematicNodes.filter((n) => n.riskScore >= 4).slice(0, 8),
|
||||||
|
[problematicNodes]
|
||||||
|
);
|
||||||
|
const warningNodes = useMemo(
|
||||||
|
() => problematicNodes.filter((n) => n.riskScore >= 1 && n.riskScore < 4).slice(0, 12),
|
||||||
|
[problematicNodes]
|
||||||
|
);
|
||||||
|
|
||||||
if (servers.length === 0) return null;
|
if (servers.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="network-map-unifi"
|
className="network-map-unifi"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 520,
|
height: isFullscreen ? '100%' : '68vh',
|
||||||
minHeight: 400,
|
minHeight: isFullscreen ? 400 : 620,
|
||||||
background: UNIFI_BG,
|
background: UNIFI_BG,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -943,5 +1042,100 @@ export default function NetworkMapUnifi({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="row g-3 mt-2">
|
||||||
|
<div className="col-12 col-xl-6">
|
||||||
|
<div
|
||||||
|
className="card"
|
||||||
|
style={{ background: '#0f172a', border: `1px solid ${UNIFI_CARD_BORDER}` }}
|
||||||
|
>
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title mb-0">Критичные узлы</h3>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter table-nowrap mb-0 table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Узел</th>
|
||||||
|
<th scope="col">Тип</th>
|
||||||
|
<th scope="col">Ping max</th>
|
||||||
|
<th scope="col">Speed min</th>
|
||||||
|
<th scope="col">Связи</th>
|
||||||
|
<th scope="col">Проблемы</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{criticalNodes.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="text-muted">Критичных узлов не найдено</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
criticalNodes.map((n) => (
|
||||||
|
<tr key={`critical-${n.ip}`}>
|
||||||
|
<td>
|
||||||
|
<div className="fw-semibold">{n.nodeLabel}</div>
|
||||||
|
<div className="text-muted small">{n.ip}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="badge bg-secondary-lt text-secondary">
|
||||||
|
{String(n.type || 'node').toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{n.maxPingMs != null ? `${n.maxPingMs} ms` : '—'}</td>
|
||||||
|
<td>{n.minSpeedMbps != null ? `${n.minSpeedMbps.toFixed(1)} Мбит/с` : '—'}</td>
|
||||||
|
<td>{n.linkCount}</td>
|
||||||
|
<td className="text-warning">{n.issues.slice(0, 2).join(', ')}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-xl-6">
|
||||||
|
<div
|
||||||
|
className="card"
|
||||||
|
style={{ background: '#0f172a', border: `1px solid ${UNIFI_CARD_BORDER}` }}
|
||||||
|
>
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title mb-0">Требуют внимания</h3>
|
||||||
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter table-nowrap mb-0 table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Узел</th>
|
||||||
|
<th scope="col">Ping max</th>
|
||||||
|
<th scope="col">Speed min</th>
|
||||||
|
<th scope="col">Stale</th>
|
||||||
|
<th scope="col">Проблемы</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{warningNodes.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5} className="text-muted">Узлов с предупреждениями не найдено</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
warningNodes.map((n) => (
|
||||||
|
<tr key={`warn-${n.ip}`}>
|
||||||
|
<td>
|
||||||
|
<div className="fw-semibold">{n.nodeLabel}</div>
|
||||||
|
<div className="text-muted small">{n.ip}</div>
|
||||||
|
</td>
|
||||||
|
<td>{n.maxPingMs != null ? `${n.maxPingMs} ms` : '—'}</td>
|
||||||
|
<td>{n.minSpeedMbps != null ? `${n.minSpeedMbps.toFixed(1)} Мбит/с` : '—'}</td>
|
||||||
|
<td>{n.staleCount}</td>
|
||||||
|
<td className="text-muted">{n.issues.slice(0, 2).join(', ')}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user