feat: Enhance GraphView with localStorage integration for node position persistence and improve edge text styling for better visualization in the topology graph.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s

This commit is contained in:
2025-12-01 16:56:27 +07:00
parent 007c7514e1
commit edbf2f4299
2 changed files with 70 additions and 2 deletions
+20
View File
@@ -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, здесь только подчёркиваем плавность */
+50 -2
View File
@@ -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);