feat: add iteration stepper to compound nodes and thread overlay props
Add a left/right stepper widget to compound node headers (LOOP, SPLIT, MULTICAST) when iteration overlay data is present. Thread executionOverlay, overlayActive, iterationState, and onIterationChange props through ProcessDiagram -> CompoundNode -> children and ProcessDiagram -> ErrorSection -> children so leaf DiagramNode instances render with execution state (green/red badges, dimming for skipped nodes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, NodeConfig>;
|
||||
/** Execution overlay for edge traversal coloring */
|
||||
executionOverlay?: Map<string, NodeExecutionState>;
|
||||
/** Whether an execution overlay is active (enables dimming of skipped nodes) */
|
||||
overlayActive?: boolean;
|
||||
/** Per-compound iteration state */
|
||||
iterationState?: Map<string, IterationInfo>;
|
||||
/** 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<string>();
|
||||
@@ -79,6 +89,27 @@ export function CompoundNode({
|
||||
{label}
|
||||
</text>
|
||||
|
||||
{/* Iteration stepper (for LOOP, SPLIT, MULTICAST with overlay data) */}
|
||||
{iterationInfo && (
|
||||
<foreignObject x={headerWidth - 80} y={1} width={75} height={20}>
|
||||
<div className={styles.iterationStepper}>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onIterationChange?.(node.id!, iterationInfo.current - 1); }}
|
||||
disabled={iterationInfo.current <= 0}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<span>{iterationInfo.current + 1} / {iterationInfo.total}</span>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onIterationChange?.(node.id!, iterationInfo.current + 1); }}
|
||||
disabled={iterationInfo.current >= iterationInfo.total - 1}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</foreignObject>
|
||||
)}
|
||||
|
||||
{/* Internal edges (rendered after background, before children) */}
|
||||
<g className="edges">
|
||||
{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)}
|
||||
|
||||
@@ -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<string, NodeConfig>;
|
||||
/** Execution overlay for edge traversal coloring */
|
||||
executionOverlay?: Map<string, NodeExecutionState>;
|
||||
/** Whether an execution overlay is active (enables dimming of skipped nodes) */
|
||||
overlayActive?: boolean;
|
||||
/** Per-compound iteration state */
|
||||
iterationState?: Map<string, IterationInfo>;
|
||||
/** 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<string, string> = {
|
||||
|
||||
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)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string[]>([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}
|
||||
|
||||
Reference in New Issue
Block a user