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 */}
{/* 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 */}
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}
))}
))}
);
}