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 { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
|
||||||
import type { NodeConfig } from './types';
|
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 { colorForType, isCompoundType } from './node-colors';
|
||||||
import { DiagramNode } from './DiagramNode';
|
import { DiagramNode } from './DiagramNode';
|
||||||
import { DiagramEdge } from './DiagramEdge';
|
import { DiagramEdge } from './DiagramEdge';
|
||||||
|
import styles from './ProcessDiagram.module.css';
|
||||||
|
|
||||||
const HEADER_HEIGHT = 22;
|
const HEADER_HEIGHT = 22;
|
||||||
const CORNER_RADIUS = 4;
|
const CORNER_RADIUS = 4;
|
||||||
@@ -20,6 +21,12 @@ interface CompoundNodeProps {
|
|||||||
nodeConfigs?: Map<string, NodeConfig>;
|
nodeConfigs?: Map<string, NodeConfig>;
|
||||||
/** Execution overlay for edge traversal coloring */
|
/** Execution overlay for edge traversal coloring */
|
||||||
executionOverlay?: Map<string, NodeExecutionState>;
|
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;
|
onNodeClick: (nodeId: string) => void;
|
||||||
onNodeDoubleClick?: (nodeId: string) => void;
|
onNodeDoubleClick?: (nodeId: string) => void;
|
||||||
onNodeEnter: (nodeId: string) => void;
|
onNodeEnter: (nodeId: string) => void;
|
||||||
@@ -29,6 +36,7 @@ interface CompoundNodeProps {
|
|||||||
export function CompoundNode({
|
export function CompoundNode({
|
||||||
node, edges, parentX = 0, parentY = 0,
|
node, edges, parentX = 0, parentY = 0,
|
||||||
selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
||||||
|
overlayActive, iterationState, onIterationChange,
|
||||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||||
}: CompoundNodeProps) {
|
}: CompoundNodeProps) {
|
||||||
const x = (node.x ?? 0) - parentX;
|
const x = (node.x ?? 0) - parentX;
|
||||||
@@ -40,6 +48,8 @@ export function CompoundNode({
|
|||||||
const color = colorForType(node.type);
|
const color = colorForType(node.type);
|
||||||
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
|
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
|
||||||
const label = node.label ? `${typeName}: ${node.label}` : typeName;
|
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
|
// Collect all descendant node IDs to filter edges that belong inside this compound
|
||||||
const descendantIds = new Set<string>();
|
const descendantIds = new Set<string>();
|
||||||
@@ -79,6 +89,27 @@ export function CompoundNode({
|
|||||||
{label}
|
{label}
|
||||||
</text>
|
</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) */}
|
{/* Internal edges (rendered after background, before children) */}
|
||||||
<g className="edges">
|
<g className="edges">
|
||||||
{internalEdges.map((edge, i) => {
|
{internalEdges.map((edge, i) => {
|
||||||
@@ -112,6 +143,9 @@ export function CompoundNode({
|
|||||||
hoveredNodeId={hoveredNodeId}
|
hoveredNodeId={hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
executionOverlay={executionOverlay}
|
executionOverlay={executionOverlay}
|
||||||
|
overlayActive={overlayActive}
|
||||||
|
iterationState={iterationState}
|
||||||
|
onIterationChange={onIterationChange}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeEnter={onNodeEnter}
|
onNodeEnter={onNodeEnter}
|
||||||
@@ -130,6 +164,8 @@ export function CompoundNode({
|
|||||||
isHovered={hoveredNodeId === child.id}
|
isHovered={hoveredNodeId === child.id}
|
||||||
isSelected={selectedNodeId === child.id}
|
isSelected={selectedNodeId === child.id}
|
||||||
config={child.id ? nodeConfigs?.get(child.id) : undefined}
|
config={child.id ? nodeConfigs?.get(child.id) : undefined}
|
||||||
|
executionState={executionOverlay?.get(child.id ?? '')}
|
||||||
|
overlayActive={overlayActive}
|
||||||
onClick={() => child.id && onNodeClick(child.id)}
|
onClick={() => child.id && onNodeClick(child.id)}
|
||||||
onDoubleClick={() => child.id && onNodeDoubleClick?.(child.id)}
|
onDoubleClick={() => child.id && onNodeDoubleClick?.(child.id)}
|
||||||
onMouseEnter={() => child.id && onNodeEnter(child.id)}
|
onMouseEnter={() => child.id && onNodeEnter(child.id)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { DiagramSection } from './types';
|
import type { DiagramSection } from './types';
|
||||||
import type { NodeConfig } 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 type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||||
import { DiagramEdge } from './DiagramEdge';
|
import { DiagramEdge } from './DiagramEdge';
|
||||||
import { DiagramNode } from './DiagramNode';
|
import { DiagramNode } from './DiagramNode';
|
||||||
@@ -19,6 +19,12 @@ interface ErrorSectionProps {
|
|||||||
nodeConfigs?: Map<string, NodeConfig>;
|
nodeConfigs?: Map<string, NodeConfig>;
|
||||||
/** Execution overlay for edge traversal coloring */
|
/** Execution overlay for edge traversal coloring */
|
||||||
executionOverlay?: Map<string, NodeExecutionState>;
|
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;
|
onNodeClick: (nodeId: string) => void;
|
||||||
onNodeDoubleClick?: (nodeId: string) => void;
|
onNodeDoubleClick?: (nodeId: string) => void;
|
||||||
onNodeEnter: (nodeId: string) => void;
|
onNodeEnter: (nodeId: string) => void;
|
||||||
@@ -32,6 +38,7 @@ const VARIANT_COLORS: Record<string, string> = {
|
|||||||
|
|
||||||
export function ErrorSection({
|
export function ErrorSection({
|
||||||
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
||||||
|
overlayActive, iterationState, onIterationChange,
|
||||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||||
}: ErrorSectionProps) {
|
}: ErrorSectionProps) {
|
||||||
const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error;
|
const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error;
|
||||||
@@ -108,6 +115,9 @@ export function ErrorSection({
|
|||||||
hoveredNodeId={hoveredNodeId}
|
hoveredNodeId={hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
executionOverlay={executionOverlay}
|
executionOverlay={executionOverlay}
|
||||||
|
overlayActive={overlayActive}
|
||||||
|
iterationState={iterationState}
|
||||||
|
onIterationChange={onIterationChange}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeEnter={onNodeEnter}
|
onNodeEnter={onNodeEnter}
|
||||||
@@ -122,6 +132,8 @@ export function ErrorSection({
|
|||||||
isHovered={hoveredNodeId === node.id}
|
isHovered={hoveredNodeId === node.id}
|
||||||
isSelected={selectedNodeId === node.id}
|
isSelected={selectedNodeId === node.id}
|
||||||
config={node.id ? nodeConfigs?.get(node.id) : undefined}
|
config={node.id ? nodeConfigs?.get(node.id) : undefined}
|
||||||
|
executionState={executionOverlay?.get(node.id ?? '')}
|
||||||
|
overlayActive={overlayActive}
|
||||||
onClick={() => node.id && onNodeClick(node.id)}
|
onClick={() => node.id && onNodeClick(node.id)}
|
||||||
onDoubleClick={() => node.id && onNodeDoubleClick?.(node.id)}
|
onDoubleClick={() => node.id && onNodeDoubleClick?.(node.id)}
|
||||||
onMouseEnter={() => node.id && onNodeEnter(node.id)}
|
onMouseEnter={() => node.id && onNodeEnter(node.id)}
|
||||||
|
|||||||
@@ -168,3 +168,36 @@
|
|||||||
background: var(--bg-hover, #F5F0EA);
|
background: var(--bg-hover, #F5F0EA);
|
||||||
color: var(--text-primary, #1A1612);
|
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,
|
className,
|
||||||
diagramLayout,
|
diagramLayout,
|
||||||
executionOverlay,
|
executionOverlay,
|
||||||
|
iterationState,
|
||||||
|
onIterationChange,
|
||||||
}: ProcessDiagramProps) {
|
}: ProcessDiagramProps) {
|
||||||
|
const overlayActive = !!executionOverlay;
|
||||||
// Route stack for drill-down navigation
|
// Route stack for drill-down navigation
|
||||||
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
|
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
|
||||||
|
|
||||||
@@ -248,6 +251,9 @@ export function ProcessDiagram({
|
|||||||
hoveredNodeId={toolbar.hoveredNodeId}
|
hoveredNodeId={toolbar.hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
executionOverlay={executionOverlay}
|
executionOverlay={executionOverlay}
|
||||||
|
overlayActive={overlayActive}
|
||||||
|
iterationState={iterationState}
|
||||||
|
onIterationChange={onIterationChange}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeEnter={toolbar.onNodeEnter}
|
onNodeEnter={toolbar.onNodeEnter}
|
||||||
@@ -262,6 +268,8 @@ export function ProcessDiagram({
|
|||||||
isHovered={toolbar.hoveredNodeId === node.id}
|
isHovered={toolbar.hoveredNodeId === node.id}
|
||||||
isSelected={selectedNodeId === node.id}
|
isSelected={selectedNodeId === node.id}
|
||||||
config={node.id ? nodeConfigs?.get(node.id) : undefined}
|
config={node.id ? nodeConfigs?.get(node.id) : undefined}
|
||||||
|
executionState={executionOverlay?.get(node.id ?? '')}
|
||||||
|
overlayActive={overlayActive}
|
||||||
onClick={() => node.id && handleNodeClick(node.id)}
|
onClick={() => node.id && handleNodeClick(node.id)}
|
||||||
onDoubleClick={() => node.id && handleNodeDoubleClick(node.id)}
|
onDoubleClick={() => node.id && handleNodeDoubleClick(node.id)}
|
||||||
onMouseEnter={() => node.id && toolbar.onNodeEnter(node.id)}
|
onMouseEnter={() => node.id && toolbar.onNodeEnter(node.id)}
|
||||||
@@ -283,6 +291,9 @@ export function ProcessDiagram({
|
|||||||
hoveredNodeId={toolbar.hoveredNodeId}
|
hoveredNodeId={toolbar.hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
executionOverlay={executionOverlay}
|
executionOverlay={executionOverlay}
|
||||||
|
overlayActive={overlayActive}
|
||||||
|
iterationState={iterationState}
|
||||||
|
onIterationChange={onIterationChange}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeEnter={toolbar.onNodeEnter}
|
onNodeEnter={toolbar.onNodeEnter}
|
||||||
|
|||||||
Reference in New Issue
Block a user