diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx index f1180c3e..2692a2bf 100644 --- a/ui/src/components/ProcessDiagram/CompoundNode.tsx +++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx @@ -1,9 +1,10 @@ import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'; import type { NodeConfig } from './types'; -import type { NodeExecutionState } from '../ExecutionDiagram/types'; +import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types'; import { colorForType, isCompoundType } 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; @@ -20,6 +21,12 @@ interface CompoundNodeProps { 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; onNodeClick: (nodeId: string) => void; onNodeDoubleClick?: (nodeId: string) => void; onNodeEnter: (nodeId: string) => void; @@ -29,6 +36,7 @@ interface CompoundNodeProps { export function CompoundNode({ node, edges, parentX = 0, parentY = 0, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay, + overlayActive, iterationState, onIterationChange, onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }: CompoundNodeProps) { const x = (node.x ?? 0) - parentX; @@ -40,6 +48,8 @@ export function CompoundNode({ 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(); @@ -79,6 +89,27 @@ export function CompoundNode({ {label} + {/* Iteration stepper (for LOOP, SPLIT, MULTICAST with overlay data) */} + {iterationInfo && ( + +
+ + {iterationInfo.current + 1} / {iterationInfo.total} + +
+
+ )} + {/* Internal edges (rendered after background, before children) */} {internalEdges.map((edge, i) => { @@ -112,6 +143,9 @@ export function CompoundNode({ hoveredNodeId={hoveredNodeId} nodeConfigs={nodeConfigs} executionOverlay={executionOverlay} + overlayActive={overlayActive} + iterationState={iterationState} + onIterationChange={onIterationChange} onNodeClick={onNodeClick} onNodeDoubleClick={onNodeDoubleClick} onNodeEnter={onNodeEnter} @@ -130,6 +164,8 @@ export function CompoundNode({ isHovered={hoveredNodeId === child.id} isSelected={selectedNodeId === child.id} config={child.id ? nodeConfigs?.get(child.id) : undefined} + executionState={executionOverlay?.get(child.id ?? '')} + overlayActive={overlayActive} onClick={() => child.id && onNodeClick(child.id)} onDoubleClick={() => child.id && onNodeDoubleClick?.(child.id)} onMouseEnter={() => child.id && onNodeEnter(child.id)} diff --git a/ui/src/components/ProcessDiagram/ErrorSection.tsx b/ui/src/components/ProcessDiagram/ErrorSection.tsx index 43448c6a..fad5c970 100644 --- a/ui/src/components/ProcessDiagram/ErrorSection.tsx +++ b/ui/src/components/ProcessDiagram/ErrorSection.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import type { DiagramSection } from './types'; import type { NodeConfig } from './types'; -import type { NodeExecutionState } from '../ExecutionDiagram/types'; +import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types'; import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; import { DiagramEdge } from './DiagramEdge'; import { DiagramNode } from './DiagramNode'; @@ -19,6 +19,12 @@ interface ErrorSectionProps { 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; onNodeClick: (nodeId: string) => void; onNodeDoubleClick?: (nodeId: string) => void; onNodeEnter: (nodeId: string) => void; @@ -32,6 +38,7 @@ const VARIANT_COLORS: Record = { export function ErrorSection({ section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay, + overlayActive, iterationState, onIterationChange, onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }: ErrorSectionProps) { const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error; @@ -108,6 +115,9 @@ export function ErrorSection({ hoveredNodeId={hoveredNodeId} nodeConfigs={nodeConfigs} executionOverlay={executionOverlay} + overlayActive={overlayActive} + iterationState={iterationState} + onIterationChange={onIterationChange} onNodeClick={onNodeClick} onNodeDoubleClick={onNodeDoubleClick} onNodeEnter={onNodeEnter} @@ -122,6 +132,8 @@ export function ErrorSection({ isHovered={hoveredNodeId === node.id} isSelected={selectedNodeId === node.id} config={node.id ? nodeConfigs?.get(node.id) : undefined} + executionState={executionOverlay?.get(node.id ?? '')} + overlayActive={overlayActive} onClick={() => node.id && onNodeClick(node.id)} onDoubleClick={() => node.id && onNodeDoubleClick?.(node.id)} onMouseEnter={() => node.id && onNodeEnter(node.id)} diff --git a/ui/src/components/ProcessDiagram/ProcessDiagram.module.css b/ui/src/components/ProcessDiagram/ProcessDiagram.module.css index c61cd5ad..c70667c0 100644 --- a/ui/src/components/ProcessDiagram/ProcessDiagram.module.css +++ b/ui/src/components/ProcessDiagram/ProcessDiagram.module.css @@ -168,3 +168,36 @@ background: var(--bg-hover, #F5F0EA); color: var(--text-primary, #1A1612); } + +.iterationStepper { + display: flex; + align-items: center; + gap: 2px; + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; + padding: 1px 3px; + font-size: 10px; + color: white; + font-family: inherit; +} + +.iterationStepper button { + width: 16px; + height: 16px; + border: none; + background: rgba(255, 255, 255, 0.2); + color: white; + border-radius: 2px; + cursor: pointer; + font-size: 10px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + font-family: inherit; +} + +.iterationStepper button:disabled { + opacity: 0.3; + cursor: default; +} diff --git a/ui/src/components/ProcessDiagram/ProcessDiagram.tsx b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx index f63b4ffe..5fcbb043 100644 --- a/ui/src/components/ProcessDiagram/ProcessDiagram.tsx +++ b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx @@ -54,7 +54,10 @@ export function ProcessDiagram({ className, diagramLayout, executionOverlay, + iterationState, + onIterationChange, }: ProcessDiagramProps) { + const overlayActive = !!executionOverlay; // Route stack for drill-down navigation const [routeStack, setRouteStack] = useState([routeId]); @@ -248,6 +251,9 @@ export function ProcessDiagram({ hoveredNodeId={toolbar.hoveredNodeId} nodeConfigs={nodeConfigs} executionOverlay={executionOverlay} + overlayActive={overlayActive} + iterationState={iterationState} + onIterationChange={onIterationChange} onNodeClick={handleNodeClick} onNodeDoubleClick={handleNodeDoubleClick} onNodeEnter={toolbar.onNodeEnter} @@ -262,6 +268,8 @@ export function ProcessDiagram({ isHovered={toolbar.hoveredNodeId === node.id} isSelected={selectedNodeId === node.id} config={node.id ? nodeConfigs?.get(node.id) : undefined} + executionState={executionOverlay?.get(node.id ?? '')} + overlayActive={overlayActive} onClick={() => node.id && handleNodeClick(node.id)} onDoubleClick={() => node.id && handleNodeDoubleClick(node.id)} onMouseEnter={() => node.id && toolbar.onNodeEnter(node.id)} @@ -283,6 +291,9 @@ export function ProcessDiagram({ hoveredNodeId={toolbar.hoveredNodeId} nodeConfigs={nodeConfigs} executionOverlay={executionOverlay} + overlayActive={overlayActive} + iterationState={iterationState} + onIterationChange={onIterationChange} onNodeClick={handleNodeClick} onNodeDoubleClick={handleNodeDoubleClick} onNodeEnter={toolbar.onNodeEnter}