feat: expose iteration/iterationSize fields for diagram overlay
Replace synthetic wrapper node approach with direct iteration fields: - ProcessorNode gains iteration (child's index) and iterationSize (container's total) fields, populated from ClickHouse flat records - Frontend hooks detect iteration containers from iterationSize != null instead of scanning for wrapper processorTypes - useExecutionOverlay filters children by iteration field instead of wrapper nodes, eliminating ITERATION_WRAPPER_TYPES entirely - Cleaner data contract: API returns exactly what the DB stores Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,18 +20,10 @@ interface ExecutionDiagramProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ITERATION_WRAPPER_TYPES = new Set([
|
||||
'loopIteration', 'splitIteration', 'multicastBranch',
|
||||
]);
|
||||
|
||||
function wrapperIndex(proc: ProcessorNode): number | undefined {
|
||||
return proc.loopIndex ?? proc.splitIndex ?? proc.multicastIndex ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a processor in the tree, respecting iteration filtering.
|
||||
* Only recurses into the selected iteration wrapper so the returned
|
||||
* ProcessorNode has data from the correct iteration.
|
||||
* Only recurses into children matching the selected iteration index
|
||||
* so the returned ProcessorNode has data from the correct iteration.
|
||||
*/
|
||||
function findProcessorInTree(
|
||||
nodes: ProcessorNode[] | undefined,
|
||||
@@ -43,18 +35,10 @@ function findProcessorInTree(
|
||||
for (const n of nodes) {
|
||||
if (!n.processorId) continue;
|
||||
|
||||
// Iteration wrapper: only recurse into the selected iteration
|
||||
if (ITERATION_WRAPPER_TYPES.has(n.processorType)) {
|
||||
if (parentId && iterationState?.has(parentId)) {
|
||||
const info = iterationState.get(parentId)!;
|
||||
const idx = wrapperIndex(n);
|
||||
if (idx != null && idx !== info.current) continue;
|
||||
}
|
||||
if (n.children) {
|
||||
const found = findProcessorInTree(n.children, processorId, iterationState, n.processorId);
|
||||
if (found) return found;
|
||||
}
|
||||
continue;
|
||||
// If parent is an iteration container, skip children from other iterations
|
||||
if (parentId && iterationState?.has(parentId)) {
|
||||
const info = iterationState.get(parentId)!;
|
||||
if (n.iteration != null && n.iteration !== info.current) continue;
|
||||
}
|
||||
|
||||
if (n.processorId === processorId) return n;
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { NodeExecutionState, IterationInfo, ProcessorNode } from './types';
|
||||
|
||||
/** Synthetic wrapper processorTypes emitted by the agent for each iteration. */
|
||||
const ITERATION_WRAPPER_TYPES = new Set([
|
||||
'loopIteration', 'splitIteration', 'multicastBranch',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Extract the iteration index from a wrapper node.
|
||||
*/
|
||||
function wrapperIndex(proc: ProcessorNode): number | undefined {
|
||||
return proc.loopIndex ?? proc.splitIndex ?? proc.multicastIndex ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walks the ProcessorNode tree and populates an overlay map
|
||||
* keyed by processorId → NodeExecutionState.
|
||||
*
|
||||
* Iteration wrappers (loopIteration, splitIteration, multicastBranch) are
|
||||
* used for filtering: only the wrapper matching the selected iteration
|
||||
* is recursed into. The wrapper itself is not added to the overlay
|
||||
* (it's synthetic and has no corresponding diagram node).
|
||||
* For iteration containers (nodes with iterationSize), only children
|
||||
* whose `iteration` matches the selected index are included.
|
||||
*/
|
||||
function buildOverlay(
|
||||
processors: ProcessorNode[],
|
||||
@@ -31,21 +17,12 @@ function buildOverlay(
|
||||
for (const proc of processors) {
|
||||
if (!proc.processorId) continue;
|
||||
|
||||
// Iteration wrapper: filter by selected iteration, skip the wrapper itself.
|
||||
// Must be checked before the status filter — wrappers may not have a status.
|
||||
if (ITERATION_WRAPPER_TYPES.has(proc.processorType)) {
|
||||
if (parentId && iterationState.has(parentId)) {
|
||||
const info = iterationState.get(parentId)!;
|
||||
const idx = wrapperIndex(proc);
|
||||
if (idx != null && idx !== info.current) {
|
||||
continue; // Skip this wrapper and all its children
|
||||
}
|
||||
// If this node's parent is an iteration container, filter by selected iteration
|
||||
if (parentId && iterationState.has(parentId)) {
|
||||
const info = iterationState.get(parentId)!;
|
||||
if (proc.iteration != null && proc.iteration !== info.current) {
|
||||
continue; // Different iteration than selected — skip this subtree
|
||||
}
|
||||
// Matching wrapper: don't add to overlay but recurse into children
|
||||
if (proc.children?.length) {
|
||||
buildOverlay(proc.children, overlay, iterationState, proc.processorId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular processor: only include completed/failed nodes
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { IterationInfo, ProcessorNode } from './types';
|
||||
|
||||
const WRAPPER_TYPES: Record<string, IterationInfo['type']> = {
|
||||
loopIteration: 'loop',
|
||||
splitIteration: 'split',
|
||||
multicastBranch: 'multicast',
|
||||
};
|
||||
/** Map container processorType to iteration display type. */
|
||||
function inferIterationType(processorType: string | undefined): IterationInfo['type'] {
|
||||
switch (processorType) {
|
||||
case 'loop': return 'loop';
|
||||
case 'multicast': return 'multicast';
|
||||
default: return 'split'; // split, recipientList, and any future iterating EIP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the processor tree and detects compound nodes that have iteration
|
||||
* wrapper children (loopIteration, splitIteration, multicastBranch).
|
||||
* Walks the processor tree and detects iteration containers —
|
||||
* any node with a non-null iterationSize.
|
||||
*/
|
||||
function detectIterations(
|
||||
processors: ProcessorNode[],
|
||||
result: Map<string, IterationInfo>,
|
||||
): void {
|
||||
for (const proc of processors) {
|
||||
if (!proc.children?.length || !proc.processorId) continue;
|
||||
if (!proc.processorId) continue;
|
||||
|
||||
// Check if children are iteration wrappers
|
||||
for (const [wrapperType, iterType] of Object.entries(WRAPPER_TYPES)) {
|
||||
const wrappers = proc.children.filter(c => c.processorType === wrapperType);
|
||||
if (wrappers.length > 0) {
|
||||
result.set(proc.processorId, {
|
||||
current: 0,
|
||||
total: wrappers.length,
|
||||
type: iterType,
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (proc.iterationSize != null) {
|
||||
result.set(proc.processorId, {
|
||||
current: 0,
|
||||
total: proc.iterationSize,
|
||||
type: inferIterationType(proc.processorType),
|
||||
});
|
||||
}
|
||||
|
||||
// Recurse into children to find nested iterations
|
||||
detectIterations(proc.children, result);
|
||||
if (proc.children?.length) {
|
||||
detectIterations(proc.children, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages per-compound iteration state for the execution overlay.
|
||||
*
|
||||
* Scans the processor tree to detect compounds with iteration wrapper
|
||||
* children and tracks which iteration index is currently selected.
|
||||
* Scans the processor tree to detect containers with iterationSize
|
||||
* and tracks which iteration index is currently selected.
|
||||
*/
|
||||
export function useIterationState(processors: ProcessorNode[] | undefined) {
|
||||
const [state, setState] = useState<Map<string, IterationInfo>>(new Map());
|
||||
|
||||
Reference in New Issue
Block a user