import { useCallback, useRef, useState } from 'react'; import type { NodeAction, NodeConfig } from '../ProcessDiagram/types'; import type { ExecutionDetail, ProcessorNode } from './types'; import { useExecutionDetail } from '../../api/queries/executions'; import { useDiagramLayout } from '../../api/queries/diagrams'; import { ProcessDiagram } from '../ProcessDiagram'; import { DetailPanel } from './DetailPanel'; import { useExecutionOverlay } from './useExecutionOverlay'; import { useIterationState } from './useIterationState'; import styles from './ExecutionDiagram.module.css'; interface ExecutionDiagramProps { executionId: string; executionDetail?: ExecutionDetail; direction?: 'LR' | 'TB'; knownRouteIds?: Set; onNodeAction?: (nodeId: string, action: NodeAction) => void; nodeConfigs?: Map; className?: string; } function findProcessorInTree( nodes: ProcessorNode[] | undefined, processorId: string | null, ): ProcessorNode | null { if (!nodes || !processorId) return null; for (const n of nodes) { if (n.processorId === processorId) return n; if (n.children) { const found = findProcessorInTree(n.children, processorId); if (found) return found; } } return null; } function findFailedProcessor(nodes: ProcessorNode[]): ProcessorNode | null { for (const n of nodes) { if (n.status === 'FAILED') return n; if (n.children) { const found = findFailedProcessor(n.children); if (found) return found; } } return null; } function statusBadgeClass(status: string): string { const s = status?.toUpperCase(); if (s === 'COMPLETED') return `${styles.statusBadge} ${styles.statusCompleted}`; if (s === 'FAILED') return `${styles.statusBadge} ${styles.statusFailed}`; if (s === 'RUNNING') return `${styles.statusBadge} ${styles.statusRunning}`; return styles.statusBadge; } export function ExecutionDiagram({ executionId, executionDetail: externalDetail, direction = 'LR', knownRouteIds, onNodeAction, nodeConfigs, className, }: ExecutionDiagramProps) { // 1. Fetch execution data (skip if pre-fetched prop provided) const detailQuery = useExecutionDetail(externalDetail ? null : executionId); const detail = externalDetail ?? detailQuery.data; const detailLoading = !externalDetail && detailQuery.isLoading; const detailError = !externalDetail && detailQuery.error; // 2. Load diagram by content hash const diagramQuery = useDiagramLayout(detail?.diagramContentHash ?? null, direction); const diagramLayout = diagramQuery.data; const diagramLoading = diagramQuery.isLoading; const diagramError = diagramQuery.error; // 3. Initialize iteration state const { iterationState, setIteration } = useIterationState(detail?.processors); // 4. Compute overlay const overlay = useExecutionOverlay(detail?.processors, iterationState); // 5. Manage selection + center-on-node const [selectedProcessorId, setSelectedProcessorId] = useState(''); const [centerOnNodeId, setCenterOnNodeId] = useState(''); // 6. Resizable splitter state const [splitPercent, setSplitPercent] = useState(60); const containerRef = useRef(null); const handleSplitterDown = useCallback((e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); const container = containerRef.current; if (!container) return; const onMove = (me: PointerEvent) => { const rect = container.getBoundingClientRect(); const y = me.clientY - rect.top; const pct = Math.min(85, Math.max(30, (y / rect.height) * 100)); setSplitPercent(pct); }; const onUp = () => { document.removeEventListener('pointermove', onMove); document.removeEventListener('pointerup', onUp); }; document.addEventListener('pointermove', onMove); document.addEventListener('pointerup', onUp); }, []); // Jump to error: find first FAILED processor, select it, and center the viewport const handleJumpToError = useCallback(() => { if (!detail?.processors) return; const failed = findFailedProcessor(detail.processors); if (failed?.processorId) { setSelectedProcessorId(failed.processorId); // Use a unique value to re-trigger centering even if the same node setCenterOnNodeId(''); requestAnimationFrame(() => setCenterOnNodeId(failed.processorId)); } }, [detail?.processors]); // Loading state if (detailLoading || (detail && diagramLoading)) { return (
Loading execution data...
); } // Error state if (detailError) { return (
Failed to load execution detail
); } if (diagramError) { return (
Failed to load diagram
); } if (!detail) { return (
No execution data
); } return (
{/* Diagram area */}
{/* Resizable splitter */}
{/* Detail panel */}
); }