import { useCallback, useEffect, useState } from 'react'; import { ReactFlow, Background, Controls, MiniMap, Node, Edge, NodeProps, Handle, Position, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import dagre from 'dagre'; import { useTopologyStore } from '../../store/topologyStore'; import { getNodeColor, getStatusColor } from '../../utils/colors'; import { Server, Network, Wifi, Box, Database, Folder } from 'lucide-react'; import { NodeType, ServiceCategory } from '../../types'; const nodeIcons: Record = { gateway: , vlan: , wifi: , host_physical: , host_vm: , host_container: , service: , volume: , mount: , path: , }; function CustomNode({ data, selected }: NodeProps) { const nodeData = data as { type?: NodeType; category?: ServiceCategory; status?: 'running' | 'stopped' | 'unknown'; label?: string; ip?: string }; const nodeColor = getNodeColor(nodeData.type || 'service', nodeData.category); const statusColor = getStatusColor(nodeData.status || 'unknown'); return (
{nodeIcons[nodeData.type || 'service']}
{nodeData.label}
{nodeData.ip && (
{nodeData.ip}
)}
); } const nodeTypes = { custom: CustomNode, }; const nodeWidth = 180; const nodeHeight = 70; function getLayoutedElements(nodes: Node[], edges: Edge[], direction: 'LR' | 'TB') { if (nodes.length === 0) return { nodes: [], edges: [] }; const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); dagreGraph.setGraph({ rankdir: direction, nodesep: 50, ranksep: 100 }); nodes.forEach((node) => { dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); }); edges.forEach((edge) => { dagreGraph.setEdge(edge.source, edge.target); }); dagre.layout(dagreGraph); const layoutedNodes = nodes.map((node) => { const nodeWithPosition = dagreGraph.node(node.id); if (!nodeWithPosition) return node; return { ...node, position: { x: nodeWithPosition.x - nodeWidth / 2, y: nodeWithPosition.y - nodeHeight / 2, }, }; }); return { nodes: layoutedNodes, edges }; } export default function TopologyGraph() { const { edges: storeEdges, selectedNodeId, setSelectedNode, getFilteredNodes, orientation, viewMode } = useTopologyStore(); const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); useEffect(() => { const filteredNodes = getFilteredNodes(); if (filteredNodes.length === 0) { setNodes([]); setEdges([]); return; } const nodeIds = new Set(filteredNodes.map(n => n.id)); const newNodes: Node[] = filteredNodes.map(node => ({ id: node.id, type: 'custom', position: { x: 0, y: 0 }, data: { label: node.name, type: node.type, status: node.data.status, category: node.data.category, ip: node.data.ip, }, selected: node.id === selectedNodeId, })); const newEdges: Edge[] = storeEdges .filter(e => nodeIds.has(e.source) && nodeIds.has(e.target)) .map(edge => ({ id: edge.id, source: edge.source, target: edge.target, type: 'smoothstep', animated: edge.source === selectedNodeId || edge.target === selectedNodeId, style: { stroke: edge.source === selectedNodeId || edge.target === selectedNodeId ? '#38BDF8' : '#475569', strokeWidth: edge.source === selectedNodeId || edge.target === selectedNodeId ? 2 : 1, }, markerEnd: { type: 'arrowclosed' as const, color: edge.source === selectedNodeId || edge.target === selectedNodeId ? '#38BDF8' : '#475569', }, })); const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements( newNodes, newEdges, orientation ); setNodes(layoutedNodes); setEdges(layoutedEdges); }, [getFilteredNodes, storeEdges, selectedNodeId, orientation, viewMode]); const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => { setSelectedNode(node.id); }, [setSelectedNode]); const onPaneClick = useCallback(() => { setSelectedNode(null); }, [setSelectedNode]); return (
getNodeColor(node.data?.type as NodeType || 'service', (node.data?.category as ServiceCategory) || undefined)} maskColor="rgba(15, 23, 42, 0.8)" />
); }