feat: add useExecutionOverlay and useIterationState hooks
useExecutionOverlay maps processor tree to overlay state map, handling iteration filtering, sub-route failure detection, and trace data flags. useIterationState detects compound nodes with iterated children and manages per-compound iteration selection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
80
ui/src/components/ExecutionDiagram/useExecutionOverlay.ts
Normal file
80
ui/src/components/ExecutionDiagram/useExecutionOverlay.ts
Normal file
@@ -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<string, NodeExecutionState>,
|
||||
iterationState: Map<string, IterationInfo>,
|
||||
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<processorId, NodeExecutionState> that tells DiagramNode
|
||||
* and DiagramEdge how to render each element (success/failure colors,
|
||||
* traversed edges, etc.).
|
||||
*/
|
||||
export function useExecutionOverlay(
|
||||
processors: ProcessorNode[] | undefined,
|
||||
iterationState: Map<string, IterationInfo>,
|
||||
): Map<string, NodeExecutionState> {
|
||||
return useMemo(() => {
|
||||
if (!processors) return new Map();
|
||||
const overlay = new Map<string, NodeExecutionState>();
|
||||
buildOverlay(processors, overlay, iterationState);
|
||||
return overlay;
|
||||
}, [processors, iterationState]);
|
||||
}
|
||||
91
ui/src/components/ExecutionDiagram/useIterationState.ts
Normal file
91
ui/src/components/ExecutionDiagram/useIterationState.ts
Normal file
@@ -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<string, IterationInfo>,
|
||||
): 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<number>();
|
||||
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<Map<string, IterationInfo>>(new Map());
|
||||
|
||||
// Initialize iteration info when processors change
|
||||
useEffect(() => {
|
||||
if (!processors) return;
|
||||
const newState = new Map<string, IterationInfo>();
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user