diff --git a/ui/src/components/ExecutionDiagram/useExecutionOverlay.ts b/ui/src/components/ExecutionDiagram/useExecutionOverlay.ts new file mode 100644 index 00000000..21736e24 --- /dev/null +++ b/ui/src/components/ExecutionDiagram/useExecutionOverlay.ts @@ -0,0 +1,80 @@ +import { useMemo } from 'react'; +import type { NodeExecutionState, IterationInfo, ProcessorNode } from './types'; + +/** + * Recursively walks the ProcessorNode tree and populates an overlay map + * keyed by processorId → NodeExecutionState. + * + * Handles iteration filtering: when a processor has a loop/split/multicast + * index, only include it if it matches the currently selected iteration + * for its parent compound node. + */ +function buildOverlay( + processors: ProcessorNode[], + overlay: Map, + iterationState: Map, + parentId?: string, +): void { + for (const proc of processors) { + if (!proc.processorId || !proc.status) continue; + if (proc.status !== 'COMPLETED' && proc.status !== 'FAILED') continue; + + // Iteration filtering: if this processor belongs to an iterated parent, + // only include it when the index matches the selected iteration. + if (parentId && iterationState.has(parentId)) { + const info = iterationState.get(parentId)!; + if (info.type === 'loop' && proc.loopIndex != null) { + if (proc.loopIndex !== info.current) { + // Still recurse into children so nested compounds are discovered, + // but skip adding this processor to the overlay. + continue; + } + } + if (info.type === 'split' && proc.splitIndex != null) { + if (proc.splitIndex !== info.current) { + continue; + } + } + if (info.type === 'multicast' && proc.multicastIndex != null) { + if (proc.multicastIndex !== info.current) { + continue; + } + } + } + + const subRouteFailed = + proc.status === 'FAILED' && + (proc.processorType?.includes('DIRECT') || proc.processorType?.includes('SEDA')); + + overlay.set(proc.processorId, { + status: proc.status as 'COMPLETED' | 'FAILED', + durationMs: proc.durationMs ?? 0, + subRouteFailed: subRouteFailed || undefined, + hasTraceData: true, + }); + + // Recurse into children, passing this processor as the parent for iteration filtering. + if (proc.children?.length) { + buildOverlay(proc.children, overlay, iterationState, proc.processorId); + } + } +} + +/** + * Maps execution data (processor tree) to diagram overlay state. + * + * Returns a Map that tells DiagramNode + * and DiagramEdge how to render each element (success/failure colors, + * traversed edges, etc.). + */ +export function useExecutionOverlay( + processors: ProcessorNode[] | undefined, + iterationState: Map, +): Map { + return useMemo(() => { + if (!processors) return new Map(); + const overlay = new Map(); + buildOverlay(processors, overlay, iterationState); + return overlay; + }, [processors, iterationState]); +} diff --git a/ui/src/components/ExecutionDiagram/useIterationState.ts b/ui/src/components/ExecutionDiagram/useIterationState.ts new file mode 100644 index 00000000..02a61c04 --- /dev/null +++ b/ui/src/components/ExecutionDiagram/useIterationState.ts @@ -0,0 +1,91 @@ +import { useCallback, useEffect, useState } from 'react'; +import type { IterationInfo, ProcessorNode } from './types'; + +/** + * Walks the processor tree and detects compound nodes that have iterated + * children (loop, split, multicast). Populates a map of compoundId → + * IterationInfo so the UI can show stepper widgets and filter iterations. + */ +function detectIterations( + processors: ProcessorNode[], + result: Map, +): void { + for (const proc of processors) { + if (!proc.children?.length) continue; + + // Check if children indicate a loop compound + const loopChild = proc.children.find( + (c) => c.loopSize != null && c.loopSize > 0, + ); + if (loopChild && proc.processorId) { + result.set(proc.processorId, { + current: 0, + total: loopChild.loopSize!, + type: 'loop', + }); + } + + // Check if children indicate a split compound + const splitChild = proc.children.find( + (c) => c.splitSize != null && c.splitSize > 0, + ); + if (splitChild && !loopChild && proc.processorId) { + result.set(proc.processorId, { + current: 0, + total: splitChild.splitSize!, + type: 'split', + }); + } + + // Check if children indicate a multicast compound + if (!loopChild && !splitChild) { + const multicastIndices = new Set(); + for (const child of proc.children) { + if (child.multicastIndex != null) { + multicastIndices.add(child.multicastIndex); + } + } + if (multicastIndices.size > 0 && proc.processorId) { + result.set(proc.processorId, { + current: 0, + total: multicastIndices.size, + type: 'multicast', + }); + } + } + + // Recurse into children to find nested iterations + detectIterations(proc.children, result); + } +} + +/** + * Manages per-compound iteration state for the execution overlay. + * + * Scans the processor tree to detect compounds with iterated children + * and tracks which iteration index is currently selected for each. + */ +export function useIterationState(processors: ProcessorNode[] | undefined) { + const [state, setState] = useState>(new Map()); + + // Initialize iteration info when processors change + useEffect(() => { + if (!processors) return; + const newState = new Map(); + detectIterations(processors, newState); + setState(newState); + }, [processors]); + + const setIteration = useCallback((compoundId: string, index: number) => { + setState((prev) => { + const next = new Map(prev); + const info = next.get(compoundId); + if (info && index >= 0 && index < info.total) { + next.set(compoundId, { ...info, current: index }); + } + return next; + }); + }, []); + + return { iterationState: state, setIteration }; +}