import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'; import type { NodeConfig, LatencyHeatmapEntry } from './types'; import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types'; import { colorForType, isCompoundType, iconForType, type IconElement } from './node-colors'; import { DiagramNode } from './DiagramNode'; import { DiagramEdge } from './DiagramEdge'; import styles from './ProcessDiagram.module.css'; const HEADER_HEIGHT = 22; const CORNER_RADIUS = 4; interface CompoundNodeProps { node: DiagramNodeType; /** All edges for this section — compound filters to its own internal edges */ edges: DiagramEdgeType[]; /** Absolute offset of the nearest compound ancestor (for coordinate adjustment) */ parentX?: number; parentY?: number; selectedNodeId?: string; hoveredNodeId: string | null; nodeConfigs?: Map; /** Execution overlay for edge traversal coloring */ executionOverlay?: Map; /** Whether an execution overlay is active (enables dimming of skipped nodes) */ overlayActive?: boolean; /** Per-compound iteration state */ iterationState?: Map; /** Called when user changes iteration on a compound stepper */ onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void; latencyHeatmap?: Map; onNodeClick: (nodeId: string) => void; onNodeDoubleClick?: (nodeId: string) => void; onNodeEnter: (nodeId: string) => void; onNodeLeave: () => void; } export function CompoundNode({ node, edges, parentX = 0, parentY = 0, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay, overlayActive, iterationState, onIterationChange, latencyHeatmap, onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }: CompoundNodeProps) { const x = (node.x ?? 0) - parentX; const y = (node.y ?? 0) - parentY; const absX = node.x ?? 0; const absY = node.y ?? 0; const w = node.width ?? 200; const h = node.height ?? 100; const color = colorForType(node.type); const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? ''; const label = node.label ? `${typeName}: ${node.label}` : typeName; const iterationInfo = node.id ? iterationState?.get(node.id) : undefined; const headerWidth = w; // Collect all descendant node IDs to filter edges that belong inside this compound const descendantIds = new Set(); collectIds(node.children ?? [], descendantIds); const internalEdges = edges.filter( e => descendantIds.has(e.sourceId) && descendantIds.has(e.targetId), ); const childProps = { edges, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay, overlayActive, iterationState, onIterationChange, latencyHeatmap, onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }; // Execution overlay state for this compound const ownState = node.id ? executionOverlay?.get(node.id) : undefined; const hasExecutedChild = ownState && hasExecutedDescendant(node, executionOverlay); const isCompleted = ownState?.status === 'COMPLETED'; const isFailed = ownState?.status === 'FAILED'; // Gated = gate processor (filter/idempotent) blocked all children from executing const isGated = ownState && !hasExecutedChild && (ownState.filterMatched === false || ownState.duplicateMessage === true); // Color priority: gated (amber) > failed (red) > completed (green) > default const effectiveColor = isGated ? 'var(--amber)' : isFailed ? '#C0392B' : isCompleted ? '#3D7C47' : color; // Dim compound when overlay is active but neither the compound nor any // descendant was executed in the current iteration. const isSkipped = overlayActive && !ownState && !hasExecutedDescendant(node, executionOverlay); // _TRY_BODY / _CB_MAIN: transparent wrapper — no header, no border, just layout if (node.type === '_TRY_BODY' || node.type === '_CB_MAIN') { return ( {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} {renderChildren(node, absX, absY, childProps)} ); } // _CB_FALLBACK: section styling with EIP purple if (node.type === '_CB_FALLBACK') { const fallbackColor = '#7C3AED'; // EIP purple return ( fallback {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} {renderChildren(node, absX, absY, childProps)} ); } // DO_CATCH / DO_FINALLY: section-like styling (tinted bg, thin border, label) if (node.type === 'DO_CATCH' || node.type === 'DO_FINALLY') { const sectionLabel = node.type === 'DO_CATCH' ? (node.label ? `catch: ${node.label}` : 'catch') : (node.label ? `finally: ${node.label}` : 'finally'); return ( {/* Tinted background */} {/* Border */} {/* Section label */} {sectionLabel} {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} {renderChildren(node, absX, absY, childProps)} ); } // Default compound rendering (DO_TRY, EIP_CHOICE, EIP_FILTER, EIP_IDEMPOTENT_CONSUMER, etc.) const containerFill = isGated ? 'var(--amber-bg)' : 'white'; return ( {/* Container body */} {/* Colored header bar */} {/* Header icon (left-aligned) */} {iconForType(node.type).map((el: IconElement, i: number) => 'd' in el ? : )} {/* Header label (centered) */} {label} {/* Iteration stepper (for LOOP, SPLIT, MULTICAST with overlay data) */} {iterationInfo && (
{iterationInfo.current + 1} / {iterationInfo.total}
)} {renderInternalEdges(internalEdges, absX, absY, executionOverlay)} {renderChildren(node, absX, absY, childProps)}
); } /** Render internal edges adjusted for compound coordinates */ function renderInternalEdges( internalEdges: DiagramEdgeType[], absX: number, absY: number, executionOverlay?: Map, ) { return ( {internalEdges.map((edge, i) => { const isTraversed = executionOverlay ? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId)) : undefined; return ( [p[0] - absX, p[1] - absY]), }} traversed={isTraversed} /> ); })} ); } /** Render children — compounds recurse, leaves become DiagramNode */ function renderChildren( node: DiagramNodeType, absX: number, absY: number, props: Omit, ) { return ( <> {node.children?.map(child => { if (isCompoundType(child.type) && child.children && child.children.length > 0) { return ( ); } return ( child.id && props.onNodeClick(child.id)} onDoubleClick={() => child.id && props.onNodeDoubleClick?.(child.id)} onMouseEnter={() => child.id && props.onNodeEnter(child.id)} onMouseLeave={props.onNodeLeave} /> ); })} ); } function collectIds(nodes: DiagramNodeType[], set: Set) { for (const n of nodes) { if (n.id) set.add(n.id); if (n.children) collectIds(n.children, set); } } function hasExecutedDescendant( node: DiagramNodeType, overlay?: Map, ): boolean { if (!overlay || !node.children) return false; for (const child of node.children) { if (child.id && overlay.has(child.id)) return true; if (child.children && hasExecutedDescendant(child, overlay)) return true; } return false; }