From 2b805ec196702909ea661bd1d1058efd24e75e05 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:44:16 +0100 Subject: [PATCH] feat: add execution overlay visual states to DiagramNode DiagramNode now accepts executionState and overlayActive props to render execution status: green tint + checkmark badge for completed nodes, red tint + exclamation badge for failed nodes, dimmed opacity for skipped nodes. Duration is shown at bottom-right, and a drill-down arrow appears for sub-route failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/ProcessDiagram/DiagramNode.tsx | 115 ++++++++++++++++-- 1 file changed, 106 insertions(+), 9 deletions(-) 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 && ( + + ↳ + + )} ); }