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;
|
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) */
|
/* Небольшие отличия по типу туннеля (на будущее, сейчас основное зашито в JS) */
|
||||||
.react-flow__edge.edge-tunnel-gre .react-flow__edge-path {
|
.react-flow__edge.edge-tunnel-gre .react-flow__edge-path {
|
||||||
/* GRE — базовый синий, уже задан в JS, здесь только подчёркиваем плавность */
|
/* GRE — базовый синий, уже задан в JS, здесь только подчёркиваем плавность */
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ function GraphView({ servers, connections, onCreateConnection }) {
|
|||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [highlightedNodeId, setHighlightedNodeId] = useState(null);
|
const [highlightedNodeId, setHighlightedNodeId] = useState(null);
|
||||||
|
const STORAGE_KEY = 'graph-layout-servers-v1';
|
||||||
|
|
||||||
const getTunnelStyle = useCallback((tunnelType) => {
|
const getTunnelStyle = useCallback((tunnelType) => {
|
||||||
const map = {
|
const map = {
|
||||||
@@ -53,15 +54,62 @@ function GraphView({ servers, connections, onCreateConnection }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const initialNodesData = useMemo(() => {
|
const initialNodesData = useMemo(() => {
|
||||||
return (servers || []).map((s, i) => ({
|
// Базовая сетка
|
||||||
|
const baseNodes = (servers || []).map((s, i) => ({
|
||||||
id: String(s.ip),
|
id: String(s.ip),
|
||||||
type: 'server',
|
type: 'server',
|
||||||
position: computeGridPosition(i, servers.length),
|
position: computeGridPosition(i, servers.length),
|
||||||
data: { server: s },
|
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]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
setNodes(initialNodesData);
|
setNodes(initialNodesData);
|
||||||
|
|||||||
Reference in New Issue
Block a user