diff --git a/frontend/src/NetworkMapDashboard.jsx b/frontend/src/NetworkMapDashboard.jsx index e422d48..62e6713 100644 --- a/frontend/src/NetworkMapDashboard.jsx +++ b/frontend/src/NetworkMapDashboard.jsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import api from './lib/api.js'; import { IconRefresh, IconTopologyRing } from '@tabler/icons-react'; import PageHeader from './components/PageHeader.jsx'; -import GraphView from './GraphView.jsx'; +import NetworkMapUnifi from './NetworkMapUnifi.jsx'; /** Параллельно выполняем промисы с лимитом одновременных */ async function runWithLimit(tasks, limit = 4) { @@ -174,7 +174,7 @@ export default function NetworkMapDashboard() { Нет серверов. Добавьте серверы и связи в разделе «Серверы». ) : ( - число (мс) + */ +export default function NetworkMapUnifi({ servers = [], connections = [], pingMap = {} }) { + const containerRef = useRef(null); + const [size, setSize] = useState({ w: 800, h: 520 }); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const ro = new ResizeObserver((entries) => { + const { width, height } = entries[0]?.contentRect ?? {}; + if (width > 0 && height > 0) setSize({ w: width, h: height }); + }); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + const { positions } = useMemo(() => { + const n = servers.length; + if (n === 0) return { positions: {} }; + const cx = size.w / 2; + const cy = size.h / 2; + const r = Math.min(CIRCLE_RADIUS, (Math.min(size.w, size.h) * 0.4)); + const positions = {}; + servers.forEach((s, i) => { + const angle = (2 * Math.PI * i) / n - Math.PI / 2; + positions[String(s.ip)] = { + x: cx + r * Math.cos(angle), + y: cy + r * Math.sin(angle), + }; + }); + return { positions }; + }, [servers, size]); + + const edgesWithPing = useMemo(() => { + return (connections || []).map((c) => { + const from = String(c.from); + const to = String(c.to); + const p1 = positions[from]; + const p2 = positions[to]; + const fwd = pingMap[`${from}:${to}`]; + const rev = pingMap[`${to}:${from}`]; + const parts = []; + if (typeof fwd === 'number') parts.push(`→ ${fwd} ms`); + if (typeof rev === 'number') parts.push(`← ${rev} ms`); + const pingLabel = parts.length ? parts.join(' ') : null; + return { + from, + to, + p1, + p2, + pingLabel, + tunnelType: c.tunnelType, + }; + }).filter((e) => e.p1 && e.p2); + }, [connections, positions, pingMap]); + + const pathD = useCallback((e) => { + const { p1, p2 } = e; + const mx = (p1.x + p2.x) / 2; + const my = (p1.y + p2.y) / 2; + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const len = Math.hypot(dx, dy) || 1; + const perpX = (-dy / len) * 35; + const perpY = (dx / len) * 35; + const cpx = mx + perpX; + const cpy = my + perpY; + return `M ${p1.x} ${p1.y} Q ${cpx} ${cpy} ${p2.x} ${p2.y}`; + }, []); + + const labelPoint = useCallback((e) => { + const { p1, p2 } = e; + const mx = (p1.x + p2.x) / 2; + const my = (p1.y + p2.y) / 2; + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const len = Math.hypot(dx, dy) || 1; + const perpX = (-dy / len) * 35; + const perpY = (dx / len) * 35; + const cpx = mx + perpX; + const cpy = my + perpY; + const t = 0.5; + const lx = (1 - t) * (1 - t) * p1.x + 2 * (1 - t) * t * cpx + t * t * p2.x; + const ly = (1 - t) * (1 - t) * p1.y + 2 * (1 - t) * t * cpy + t * t * p2.y; + return { x: lx, y: ly }; + }, []); + + if (servers.length === 0) return null; + + return ( +
+ + {/* Рёбра: зелёные кривые (стиль UNIFI — проводные соединения) */} + + {edgesWithPing.map((e, idx) => ( + + + {e.pingLabel && (() => { + const lp = labelPoint(e); + const pad = 6; + const w = e.pingLabel.length * 6 + pad * 2; + const h = 18; + return ( + + + + {e.pingLabel} + + + ); + })()} + + ))} + + + {/* Узлы: карточки серверов в стиле UNIFI */} + {servers.map((s) => { + const pos = positions[String(s.ip)]; + if (!pos) return null; + const label = s.dns?.split('.')[0] || s.ip || '?'; + return ( + + + + {/* Иконка сервера в стиле UNIFI: прямоугольник с «портами» */} + + + + + + + {label.length > 12 ? label.slice(0, 11) + '…' : label} + + + {s.ip} + + + + ); + })} + +
+ ); +}