import React, { useState, useMemo } from 'react'; import { Network, Activity, ArrowRight, Zap, Target, BookOpen, FileCode } from 'lucide-react'; export default function GraphViewer({ graphData }) { const [selectedNode, setSelectedNode] = useState(null); const [activeFlow, setActiveFlow] = useState(null); const [activePath, setActivePath] = useState(null); const [hoveredNode, setHoveredNode] = useState(null); const width = 800; const height = 500; // 1. Process nodes and assign architectural layers (X-coordinates) const layoutData = useMemo(() => { if (!graphData || !graphData.nodes) return { nodes: [], edges: [] }; const nodes = [...graphData.nodes]; const edges = [...graphData.edges]; // Group nodes by type to layer them const layers = { entrypoint: [], api: [], module: [], file: [], database: [], other: [] }; nodes.forEach(node => { // Normalise type checks const type = (node.type || 'file').toLowerCase(); if (layers[type]) { layers[type].push(node); } else { layers['other'].push(node); } }); // Map layer to an X coordinate const layerX = { entrypoint: 100, api: 240, module: 440, file: 440, // Combine modules and files in the middle database: 660, other: 550 }; const nodePositions = {}; // Position nodes evenly in Y for each layer Object.entries(layers).forEach(([type, layerNodes]) => { const x = layerX[type] || 380; const count = layerNodes.length; layerNodes.forEach((node, index) => { // Distribute Y values evenly const y = count === 1 ? height / 2 : ((index + 0.5) / count) * height; nodePositions[node.id] = { ...node, x, y, color: getNodeColor(node.type), }; }); }); return { nodes: Object.values(nodePositions), edges: edges.map(edge => ({ ...edge, sourceNode: nodePositions[edge.source], targetNode: nodePositions[edge.target] })).filter(edge => edge.sourceNode && edge.targetNode) }; }, [graphData]); // Color mapping based on node type function getNodeColor(type) { switch (type?.toLowerCase()) { case 'entrypoint': return '#f97316'; // Neon Orange case 'api': return '#10b981'; // Neon Emerald case 'database': return '#a855f7'; // Purple case 'module': return '#00f2fe'; // Neon Cyan case 'file': return '#3b82f6'; // Bright Blue default: return '#94a3b8'; // Slate } } // Check if link or node is highlighted by selected business flow or critical path const highlightedNodeIds = useMemo(() => { if (activeFlow) { const flow = graphData.business_flows.find(f => f.flow_name === activeFlow); return new Set(flow?.steps || []); } if (activePath) { const path = graphData.critical_paths.find(p => p.path_name === activePath); return new Set(path?.nodes || []); } return null; }, [activeFlow, activePath, graphData]); const handleNodeClick = (node) => { setSelectedNode(node); }; const clearSelection = () => { setSelectedNode(null); setActiveFlow(null); setActivePath(null); }; if (!graphData || !graphData.nodes || graphData.nodes.length === 0) { return (
No relationship graph metadata available.
); } return (
{/* Graph Visualiser SVG Panel */}
{/* Arrow Head markers for directional lines */} {/* Link lines */} {layoutData.edges.map((edge, idx) => { const { sourceNode, targetNode } = edge; // Draw a smooth quadratic Bezier curve const dx = targetNode.x - sourceNode.x; const dy = targetNode.y - sourceNode.y; const cx = sourceNode.x + dx / 2; const cy = sourceNode.y + dy / 2 - (dx > 0 ? 30 : -30); // Curve offset const isEdgeHighlighted = highlightedNodeIds ? highlightedNodeIds.has(edge.source) && highlightedNodeIds.has(edge.target) : false; // Dim link lines if another node/path is hovered/active let strokeOpacity = 0.25; if (hoveredNode) { const isConnected = edge.source === hoveredNode || edge.target === hoveredNode; strokeOpacity = isConnected ? 0.8 : 0.05; } else if (highlightedNodeIds) { strokeOpacity = isEdgeHighlighted ? 0.9 : 0.05; } return ( {/* Subtle label hover */} {isEdgeHighlighted && ( {edge.label} )} ); })} {/* Node elements */} {layoutData.nodes.map((node) => { const isNodeHighlighted = highlightedNodeIds ? highlightedNodeIds.has(node.id) : true; // Calculate focus opacity let nodeOpacity = 1; if (hoveredNode) { const isSelf = node.id === hoveredNode; const isNeighbour = layoutData.edges.some( e => (e.source === hoveredNode && e.target === node.id) || (e.target === hoveredNode && e.source === node.id) ); nodeOpacity = (isSelf || isNeighbour) ? 1 : 0.15; } else if (highlightedNodeIds) { nodeOpacity = isNodeHighlighted ? 1 : 0.15; } const isSelected = selectedNode?.id === node.id; return ( handleNodeClick(node)} onMouseEnter={() => setHoveredNode(node.id)} onMouseLeave={() => setHoveredNode(null)} className="node-circle" > {/* Outer ring for selected node */} {isSelected && ( )} {/* Colored center node */} {/* Node Title text */} {node.label} ); })} {/* Graph Legend */}
Entry Points
APIs
Modules / Code
Databases / Storage
{/* Selected Node Details Card */} {selectedNode ? (

{selectedNode.label}

Type: {selectedNode.type}

{selectedNode.properties?.path && (

{selectedNode.properties.path}

)} {selectedNode.properties?.db_type && (

DB Technology: {selectedNode.properties.db_type}

)}
) : (
Hover over nodes to inspect dependencies. Click a node to view properties.
)}
{/* Sidebar: Business Flows, Critical Paths, Concepts lists */}
{/* Business Flows Panel */}
Business Flows

Sequence steps mapping end-to-end user operations. Click to trace path in the graph.

{graphData.business_flows?.map((flow, idx) => (
{ setActiveFlow(activeFlow === flow.flow_name ? null : flow.flow_name); setActivePath(null); }} style={{ padding: '0.75rem', borderRadius: 'var(--radius-sm)', background: activeFlow === flow.flow_name ? 'rgba(0, 242, 254, 0.08)' : 'rgba(255, 255, 255, 0.02)', border: `1px solid ${activeFlow === flow.flow_name ? 'var(--accent-cyan)' : 'var(--border-color)'}`, cursor: 'pointer', transition: 'all 0.2s' }} >
{flow.flow_name}
{flow.description}
{activeFlow === flow.flow_name && (
{flow.steps.map((step, sIdx) => ( {step.split('/').pop()} {sIdx < flow.steps.length - 1 && } ))}
)}
))}
{/* Critical Paths Panel */}
Critical Paths
{graphData.critical_paths?.map((path, idx) => (
{ setActivePath(activePath === path.path_name ? null : path.path_name); setActiveFlow(null); }} style={{ padding: '0.75rem', borderRadius: 'var(--radius-sm)', background: activePath === path.path_name ? 'rgba(249, 115, 22, 0.08)' : 'rgba(255, 255, 255, 0.02)', border: `1px solid ${activePath === path.path_name ? 'var(--accent-orange)' : 'var(--border-color)'}`, cursor: 'pointer', transition: 'all 0.2s' }} >
{path.path_name}
{path.description}
))}
{/* Concepts list */}
Code Concepts
{graphData.concepts?.map((concept, idx) => (
{concept.name}
{concept.description}
{concept.files.map((file, fIdx) => (
{file}
))}
))}
); }