From edbf2f4299782123b5004a66e2237df634e853c2 Mon Sep 17 00:00:00 2001 From: shats Date: Mon, 1 Dec 2025 16:56:27 +0700 Subject: [PATCH] feat: Enhance GraphView with localStorage integration for node position persistence and improve edge text styling for better visualization in the topology graph. --- frontend/src/App.css | 20 +++++++++++++++ frontend/src/GraphView.jsx | 52 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index c0aea78..6d4d700 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -541,6 +541,26 @@ button:focus-visible, stroke-opacity: 0.9; } +/* Подписи к рёбрам: не обрезаем, делаем «таблетку» поверх линий */ +.react-flow__edge-textwrapper { + overflow: visible; +} + +.react-flow__edge-textbg { + fill: #ffffff; + stroke: #e5e7eb; + stroke-width: 1; + rx: 999px; + ry: 999px; +} + +.react-flow__edge-text { + font-size: 11px; + font-weight: 500; + fill: #0f172a; + white-space: nowrap; +} + /* Небольшие отличия по типу туннеля (на будущее, сейчас основное зашито в JS) */ .react-flow__edge.edge-tunnel-gre .react-flow__edge-path { /* GRE — базовый синий, уже задан в JS, здесь только подчёркиваем плавность */ diff --git a/frontend/src/GraphView.jsx b/frontend/src/GraphView.jsx index b50683d..c3f7f41 100644 --- a/frontend/src/GraphView.jsx +++ b/frontend/src/GraphView.jsx @@ -20,6 +20,7 @@ function GraphView({ servers, connections, onCreateConnection }) { const containerRef = useRef(null); const [isFullscreen, setIsFullscreen] = useState(false); const [highlightedNodeId, setHighlightedNodeId] = useState(null); + const STORAGE_KEY = 'graph-layout-servers-v1'; const getTunnelStyle = useCallback((tunnelType) => { const map = { @@ -53,15 +54,62 @@ function GraphView({ servers, connections, onCreateConnection }) { }, []); const initialNodesData = useMemo(() => { - return (servers || []).map((s, i) => ({ + // Базовая сетка + const baseNodes = (servers || []).map((s, i) => ({ id: String(s.ip), type: 'server', position: computeGridPosition(i, servers.length), data: { server: s }, })); + + // Переопределяем позиции из localStorage, если есть + if (typeof window !== 'undefined') { + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (raw) { + const saved = JSON.parse(raw) || {}; + return baseNodes.map((n) => { + const savedPos = saved[n.id]; + if (savedPos && typeof savedPos.x === 'number' && typeof savedPos.y === 'number') { + return { ...n, position: { x: savedPos.x, y: savedPos.y } }; + } + return n; + }); + } + } catch { + // игнорируем проблемы с localStorage + } + } + + return baseNodes; }, [servers, computeGridPosition]); - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodesData); + const [nodes, setNodes, onNodesChangeInternal] = useNodesState(initialNodesData); + + // Обёртка над onNodesChange: обновляем состояние и сохраняем позиции в localStorage + const onNodesChange = useCallback( + (changes) => { + onNodesChangeInternal(changes); + // Сохраняем только позиции + setNodes((current) => { + if (typeof window !== 'undefined') { + try { + const layout = {}; + current.forEach((n) => { + if (n.position) { + layout[n.id] = { x: n.position.x, y: n.position.y }; + } + }); + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(layout)); + } catch { + // ignore + } + } + return current; + }); + }, + [onNodesChangeInternal, setNodes] + ); useEffect(() => { setNodes(initialNodesData);