import { useEffect, useRef } from 'react'; import { useSchemaStore } from '../../store/schemaStore.ts'; import { initGraph } from './graph/initGraph.ts'; import { registerAllNodes } from './graph/registerNodes.ts'; import { buildGraphData } from './helpers/dataMapper.ts'; import { mockData } from '../../mock/schemaData.ts'; import { DeviceGroup } from '../../types/index.ts'; let nodesRegistered = false; export function SchemaCanvas() { const containerRef = useRef(null); const minimapRef = useRef(null); const setGraph = useSchemaStore((state) => state.setGraph); const setContextMenu = useSchemaStore((state) => state.setContextMenu); const setRightPanelData = useSchemaStore((state) => state.setRightPanelData); const setConnectionsPanelData = useSchemaStore( (state) => state.setConnectionsPanelData, ); const setConnectionsPanelVisible = useSchemaStore( (state) => state.setConnectionsPanelVisible, ); const displaySettings = useSchemaStore((state) => state.displaySettings); useEffect(() => { if (!containerRef.current) return; if (!nodesRegistered) { registerAllNodes(); nodesRegistered = true; } const graph = initGraph(containerRef.current, minimapRef.current); setGraph(graph); const { nodes, edges } = buildGraphData(mockData, displaySettings.lineType); // Add nodes first (sites, then devices, then cards) const siteNodes = nodes.filter((n) => n.shape === 'site-node'); const deviceNodes = nodes.filter( (n) => n.shape !== 'site-node' && n.shape !== 'card-node', ); const cardNodes = nodes.filter((n) => n.shape === 'card-node'); for (const node of siteNodes) { graph.addNode(node); } for (const node of deviceNodes) { const graphNode = graph.addNode(node); // Set parent (embed in site) if (node.parent) { const parentNode = graph.getCellById(node.parent); if (parentNode) { parentNode.addChild(graphNode); } } } for (const node of cardNodes) { const graphNode = graph.addNode(node); if (node.parent) { const parentNode = graph.getCellById(node.parent); if (parentNode) { parentNode.addChild(graphNode); } } } // Add edges for (const edge of edges) { graph.addEdge(edge); } // Center content graph.centerContent(); // Event handlers graph.on('node:click', ({ node }) => { const data = node.getData() as Record; setRightPanelData(data); }); graph.on('edge:click', ({ edge }) => { const data = edge.getData() as Record; const line = mockData.lines.find((l) => l.id === edge.id); if (line) { setRightPanelData({ entityType: 'line', entityId: line.id, name: line.name, status: line.status, medium: line.medium, lineStyle: line.lineStyle, type: line.type, }); } else if (data) { setRightPanelData(data); } }); graph.on('node:contextmenu', ({ e, node }) => { e.preventDefault(); const data = node.getData() as Record; const entityType = data.entityType as string; let menuType: 'site' | 'active-device' | 'passive-device' = 'active-device'; if (entityType === 'site') { menuType = 'site'; } else if (entityType === 'device') { const group = data.group as string; menuType = group === DeviceGroup.Passive ? 'passive-device' : 'active-device'; } setContextMenu({ visible: true, x: e.clientX, y: e.clientY, type: menuType, data: data, }); }); graph.on('edge:contextmenu', ({ e, edge }) => { e.preventDefault(); const line = mockData.lines.find((l) => l.id === edge.id); setContextMenu({ visible: true, x: e.clientX, y: e.clientY, type: 'line', data: line ? { entityType: 'line', entityId: line.id, name: line.name, status: line.status, } : {}, }); }); graph.on('blank:contextmenu', ({ e }) => { e.preventDefault(); setContextMenu({ visible: true, x: e.clientX, y: e.clientY, type: 'blank', data: {}, }); }); graph.on('blank:click', () => { setContextMenu(null); setRightPanelData(null); }); // Port side recalculation on node move graph.on('node:moved', () => { // In a full implementation, we'd recalculate port sides here }); // Show connections panel on edge double click graph.on('edge:dblclick', ({ edge }) => { const line = mockData.lines.find((l) => l.id === edge.id); if (line) { const portA = mockData.ports.find((p) => p.id === line.portAId); const portZ = mockData.ports.find((p) => p.id === line.portZId); const devA = portA ? mockData.devices.find((d) => d.id === portA.deviceId) : null; const devZ = portZ ? mockData.devices.find((d) => d.id === portZ.deviceId) : null; setConnectionsPanelData({ line, portA, portZ, deviceA: devA, deviceZ: devZ, }); setConnectionsPanelVisible(true); } }); return () => { graph.dispose(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Sync display settings: grid useEffect(() => { const graph = useSchemaStore.getState().graph; if (!graph) return; if (displaySettings.showGrid) { graph.showGrid(); } else { graph.hideGrid(); } }, [displaySettings.showGrid]); // Sync display settings: labels useEffect(() => { const graph = useSchemaStore.getState().graph; if (!graph) return; const show = displaySettings.showLabels; // Toggle edge labels for (const edge of graph.getEdges()) { const labels = edge.getLabels(); if (labels.length > 0) { edge.setLabels( labels.map((label) => ({ ...label, attrs: { ...label.attrs, label: { ...(label.attrs?.label as Record), visibility: show ? 'visible' : 'hidden', }, rect: { ...(label.attrs?.rect as Record), visibility: show ? 'visible' : 'hidden', }, }, })), ); } } // Toggle port labels for (const node of graph.getNodes()) { const ports = node.getPorts(); for (const port of ports) { node.setPortProp(port.id!, 'attrs/text/visibility', show ? 'visible' : 'hidden'); } } }, [displaySettings.showLabels]); return (
); }