import { useCallback, useEffect } from 'react'; import type { ProcessDiagramProps } from './types'; import { useDiagramData } from './useDiagramData'; import { useZoomPan } from './useZoomPan'; import { useToolbarHover, NodeToolbar } from './NodeToolbar'; import { DiagramNode } from './DiagramNode'; import { DiagramEdge } from './DiagramEdge'; import { CompoundNode } from './CompoundNode'; import { ErrorSection } from './ErrorSection'; import { ZoomControls } from './ZoomControls'; import { isCompoundType } from './node-colors'; import styles from './ProcessDiagram.module.css'; const PADDING = 40; export function ProcessDiagram({ application, routeId, direction = 'LR', selectedNodeId, onNodeSelect, onNodeAction, nodeConfigs, className, }: ProcessDiagramProps) { const { sections, totalWidth, totalHeight, isLoading, error } = useDiagramData( application, routeId, direction, ); const zoom = useZoomPan(); const toolbar = useToolbarHover(); const contentWidth = totalWidth + PADDING * 2; const contentHeight = totalHeight + PADDING * 2; // Fit to view on first data load useEffect(() => { if (totalWidth > 0 && totalHeight > 0) { zoom.fitToView(contentWidth, contentHeight); } }, [totalWidth, totalHeight]); // eslint-disable-line react-hooks/exhaustive-deps const handleNodeClick = useCallback( (nodeId: string) => { onNodeSelect?.(nodeId); }, [onNodeSelect], ); const handleNodeAction = useCallback( (nodeId: string, action: import('./types').NodeAction) => { if (action === 'inspect') onNodeSelect?.(nodeId); onNodeAction?.(nodeId, action); }, [onNodeSelect, onNodeAction], ); const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Escape') { onNodeSelect?.(''); return; } zoom.onKeyDown(e, contentWidth, contentHeight); }, [onNodeSelect, zoom, contentWidth, contentHeight], ); if (isLoading) { return (
Loading diagram...
); } if (error) { return (
Failed to load diagram
); } if (sections.length === 0) { return (
No diagram data available
); } const mainSection = sections[0]; const errorSections = sections.slice(1); return (
onNodeSelect?.('')} > {/* Main section edges */} {mainSection.edges.map((edge, i) => ( ))} {/* Main section nodes */} {mainSection.nodes.map(node => { if (isCompoundType(node.type) && node.children && node.children.length > 0) { return ( ); } return ( node.id && handleNodeClick(node.id)} onMouseEnter={() => node.id && toolbar.onNodeEnter(node.id)} onMouseLeave={toolbar.onNodeLeave} /> ); })} {/* Toolbar for hovered node */} {toolbar.hoveredNodeId && onNodeAction && (() => { const hNode = findNodeById(sections, toolbar.hoveredNodeId!); if (!hNode) return null; return ( ); })()} {/* Error handler sections */} {errorSections.map((section, i) => ( ))} zoom.fitToView(contentWidth, contentHeight)} scale={zoom.state.scale} />
); } function findNodeById( sections: import('./types').DiagramSection[], nodeId: string, ): import('../../api/queries/diagrams').DiagramNode | undefined { for (const section of sections) { for (const node of section.nodes) { if (node.id === nodeId) return node; if (node.children) { const child = node.children.find(c => c.id === nodeId); if (child) return child; } } } return undefined; }