import { useCallback, useEffect, useRef, useState } from 'react'; import type { Network } from 'vis-network/standalone'; import { DataSet } from 'vis-data'; import Button from '@/components/ui/Button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; interface GraphNode { id: string; label: string; type: string; properties: Record; } interface GraphEdge { id: string; source: string; target: string; type: string; properties?: Record; } interface GraphData { nodes: GraphNode[]; edges: GraphEdge[]; } interface NetworkGraphProps { organizationId: string; organizationName: string; depth?: number; onNodeClick?: (nodeId: string, nodeType: string) => void; } export function NetworkGraph({ organizationId, organizationName, depth = 2, onNodeClick, }: NetworkGraphProps) { const containerRef = useRef(null); const networkRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [graphData, setGraphData] = useState(null); const [currentDepth, setCurrentDepth] = useState(depth); const fetchNetworkData = useCallback( async (depthLevel: number) => { setIsLoading(true); setError(null); try { const response = await fetch( `/api/graph/organizations/${organizationId}/network?depth=${depthLevel}` ); if (!response.ok) { throw new Error('Failed to fetch network data'); } const data = await response.json(); setGraphData(data.graph); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load network graph'); console.error('Network graph error:', err); } finally { setIsLoading(false); } }, [organizationId] ); useEffect(() => { fetchNetworkData(currentDepth); }, [fetchNetworkData, currentDepth]); useEffect(() => { if (!graphData || !containerRef.current) return; // Dynamically import vis-network and vis-data only when needed Promise.all([ import('vis-network'), import('vis-data') ]) .then(([{ Network }, { DataSet }]) => { const nodes = new DataSet( graphData.nodes.map((node) => ({ id: node.id, label: node.label, title: `${node.type}: ${node.label}`, color: getNodeColor(node.type), shape: getNodeShape(node.type), font: { size: 14, color: '#334155', }, })) ); const edges = new DataSet( graphData.edges.map((edge) => ({ id: edge.id, from: edge.source, to: edge.target, label: edge.type.replace('_', ' '), arrows: 'to', color: { color: '#94a3b8', highlight: '#3b82f6', }, font: { size: 10, align: 'middle', }, })) ); const data = { nodes, edges }; const options = { physics: { enabled: true, stabilization: { enabled: true, iterations: 100, }, barnesHut: { gravitationalConstant: -2000, centralGravity: 0.3, springLength: 150, springConstant: 0.04, damping: 0.09, }, }, interaction: { hover: true, tooltipDelay: 100, navigationButtons: true, keyboard: true, }, nodes: { borderWidth: 2, borderWidthSelected: 3, size: 25, }, edges: { width: 2, smooth: { enabled: true, type: 'continuous', roundness: 0.5, }, }, }; if (networkRef.current) { networkRef.current.destroy(); } networkRef.current = new Network(containerRef.current!, data, options); // Handle node clicks networkRef.current.on('click', (params) => { if (params.nodes.length > 0) { const nodeId = params.nodes[0] as string; const node = graphData.nodes.find((n) => n.id === nodeId); if (node && onNodeClick) { onNodeClick(node.id, node.type); } } }); // Highlight the source organization - only if the node exists in the graph const nodeExists = graphData.nodes.some((n) => n.id === organizationId); if (nodeExists) { networkRef.current.selectNodes([organizationId]); } }) .catch((err) => { console.error('Failed to load vis-network or vis-data:', err); setError('Graph visualization libraries not loaded. Run: npm install vis-network vis-data'); }); return () => { if (networkRef.current) { networkRef.current.destroy(); networkRef.current = null; } }; }, [graphData, organizationId, onNodeClick]); const getNodeColor = (type: string): string => { const colors: Record = { Organization: '#3b82f6', Site: '#10b981', ResourceFlow: '#f59e0b', Match: '#8b5cf6', SharedAsset: '#ec4899', }; return colors[type] || '#6b7280'; }; const getNodeShape = (type: string): string => { const shapes: Record = { Organization: 'dot', Site: 'square', ResourceFlow: 'diamond', Match: 'star', SharedAsset: 'triangle', }; return shapes[type] || 'dot'; }; const handleDepthChange = (newDepth: number) => { setCurrentDepth(newDepth); }; return ( Network Graph

Explore how {organizationName} connects to other organizations, sites, and resources

{[1, 2, 3].map((d) => ( ))}
{error && (

Error loading network graph

{error}

)} {isLoading && (

Loading network graph...

)} {!isLoading && !error && graphData && ( <>
{graphData.nodes.length} nodes {graphData.edges.length} connections
Organization
Site
Resource
)} ); }