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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
@@ -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, здесь только подчёркиваем плавность */
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user