diff --git a/ui/src/components/ProcessDiagram/DiagramNode.tsx b/ui/src/components/ProcessDiagram/DiagramNode.tsx index 1fd03d92..ec21587b 100644 --- a/ui/src/components/ProcessDiagram/DiagramNode.tsx +++ b/ui/src/components/ProcessDiagram/DiagramNode.tsx @@ -1,5 +1,6 @@ import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; import type { NodeConfig } from './types'; +import type { NodeExecutionState } from '../ExecutionDiagram/types'; import { colorForType, iconForType } from './node-colors'; import { ConfigBadge } from './ConfigBadge'; @@ -11,14 +12,23 @@ interface DiagramNodeProps { isHovered: boolean; isSelected: boolean; config?: NodeConfig; + executionState?: NodeExecutionState; + overlayActive?: boolean; onClick: () => void; onDoubleClick?: () => void; onMouseEnter: () => void; onMouseLeave: () => void; } +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + return `${(ms / 1000).toFixed(1)}s`; +} + export function DiagramNode({ - node, isHovered, isSelected, config, onClick, onDoubleClick, onMouseEnter, onMouseLeave, + node, isHovered, isSelected, config, + executionState, overlayActive, + onClick, onDoubleClick, onMouseEnter, onMouseLeave, }: DiagramNodeProps) { const x = node.x ?? 0; const y = node.y ?? 0; @@ -31,6 +41,33 @@ export function DiagramNode({ const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? ''; const detail = node.label || ''; + // Overlay state derivation + const isCompleted = executionState?.status === 'COMPLETED'; + const isFailed = executionState?.status === 'FAILED'; + const isSkipped = overlayActive && !executionState; + + // Colors based on execution state + let cardFill = isHovered ? '#F5F0EA' : 'white'; + let borderStroke = isHovered || isSelected ? color : '#E4DFD8'; + let borderWidth = isHovered || isSelected ? 1.5 : 1; + let topBarColor = color; + let labelColor = '#1A1612'; + + if (isCompleted) { + cardFill = isHovered ? '#E4F5E6' : '#F0F9F1'; + borderStroke = '#3D7C47'; + borderWidth = 1.5; + topBarColor = '#3D7C47'; + } else if (isFailed) { + cardFill = isHovered ? '#F9E4E1' : '#FDF2F0'; + borderStroke = '#C0392B'; + borderWidth = 2; + topBarColor = '#C0392B'; + labelColor = '#C0392B'; + } + + const statusColor = isCompleted ? '#3D7C47' : isFailed ? '#C0392B' : undefined; + return ( {/* Selection ring */} {isSelected && ( @@ -62,34 +100,93 @@ export function DiagramNode({ width={w} height={h} rx={CORNER_RADIUS} - fill={isHovered ? '#F5F0EA' : 'white'} - stroke={isHovered || isSelected ? color : '#E4DFD8'} - strokeWidth={isHovered || isSelected ? 1.5 : 1} + fill={cardFill} + stroke={borderStroke} + strokeWidth={borderWidth} /> {/* Colored top bar */} - - + + {/* Icon */} - + {icon} {/* Type name */} - + {typeName} {/* Detail label (truncated) */} {detail && detail !== typeName && ( - + {detail.length > 22 ? detail.slice(0, 20) + '...' : detail} )} {/* Config badges */} {config && } + + {/* Execution overlay: status badge at top-right */} + {isCompleted && ( + <> + + + ✓ + + + )} + {isFailed && ( + <> + + + ! + + + )} + + {/* Execution overlay: duration text at bottom-right */} + {executionState && statusColor && ( + + {formatDuration(executionState.durationMs)} + + )} + + {/* Sub-route failure: drill-down arrow at bottom-left */} + {isFailed && executionState?.subRouteFailed && ( + + ↳ + + )} ); }