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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||||
import type { NodeConfig } from './types';
|
import type { NodeConfig } from './types';
|
||||||
|
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||||
import { colorForType, iconForType } from './node-colors';
|
import { colorForType, iconForType } from './node-colors';
|
||||||
import { ConfigBadge } from './ConfigBadge';
|
import { ConfigBadge } from './ConfigBadge';
|
||||||
|
|
||||||
@@ -11,14 +12,23 @@ interface DiagramNodeProps {
|
|||||||
isHovered: boolean;
|
isHovered: boolean;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
config?: NodeConfig;
|
config?: NodeConfig;
|
||||||
|
executionState?: NodeExecutionState;
|
||||||
|
overlayActive?: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
onMouseEnter: () => void;
|
onMouseEnter: () => void;
|
||||||
onMouseLeave: () => void;
|
onMouseLeave: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDuration(ms: number): string {
|
||||||
|
if (ms < 1000) return `${ms}ms`;
|
||||||
|
return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
|
||||||
export function DiagramNode({
|
export function DiagramNode({
|
||||||
node, isHovered, isSelected, config, onClick, onDoubleClick, onMouseEnter, onMouseLeave,
|
node, isHovered, isSelected, config,
|
||||||
|
executionState, overlayActive,
|
||||||
|
onClick, onDoubleClick, onMouseEnter, onMouseLeave,
|
||||||
}: DiagramNodeProps) {
|
}: DiagramNodeProps) {
|
||||||
const x = node.x ?? 0;
|
const x = node.x ?? 0;
|
||||||
const y = node.y ?? 0;
|
const y = node.y ?? 0;
|
||||||
@@ -31,6 +41,33 @@ export function DiagramNode({
|
|||||||
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
|
const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? '';
|
||||||
const detail = node.label || '';
|
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 (
|
return (
|
||||||
<g
|
<g
|
||||||
data-node-id={node.id}
|
data-node-id={node.id}
|
||||||
@@ -40,6 +77,7 @@ export function DiagramNode({
|
|||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
|
opacity={isSkipped ? 0.35 : undefined}
|
||||||
>
|
>
|
||||||
{/* Selection ring */}
|
{/* Selection ring */}
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
@@ -62,34 +100,93 @@ export function DiagramNode({
|
|||||||
width={w}
|
width={w}
|
||||||
height={h}
|
height={h}
|
||||||
rx={CORNER_RADIUS}
|
rx={CORNER_RADIUS}
|
||||||
fill={isHovered ? '#F5F0EA' : 'white'}
|
fill={cardFill}
|
||||||
stroke={isHovered || isSelected ? color : '#E4DFD8'}
|
stroke={borderStroke}
|
||||||
strokeWidth={isHovered || isSelected ? 1.5 : 1}
|
strokeWidth={borderWidth}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Colored top bar */}
|
{/* Colored top bar */}
|
||||||
<rect x={0} y={0} width={w} height={TOP_BAR_HEIGHT} rx={CORNER_RADIUS} fill={color} />
|
<rect x={0} y={0} width={w} height={TOP_BAR_HEIGHT} rx={CORNER_RADIUS} fill={topBarColor} />
|
||||||
<rect x={CORNER_RADIUS} y={0} width={w - CORNER_RADIUS * 2} height={TOP_BAR_HEIGHT} fill={color} />
|
<rect x={CORNER_RADIUS} y={0} width={w - CORNER_RADIUS * 2} height={TOP_BAR_HEIGHT} fill={topBarColor} />
|
||||||
|
|
||||||
{/* Icon */}
|
{/* Icon */}
|
||||||
<text x={14} y={h / 2 + 6} fill={color} fontSize={14}>
|
<text x={14} y={h / 2 + 6} fill={statusColor ?? color} fontSize={14}>
|
||||||
{icon}
|
{icon}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
{/* Type name */}
|
{/* Type name */}
|
||||||
<text x={32} y={h / 2 + 1} fill="#1A1612" fontSize={11} fontWeight={600}>
|
<text x={32} y={h / 2 + 1} fill={labelColor} fontSize={11} fontWeight={600}>
|
||||||
{typeName}
|
{typeName}
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
{/* Detail label (truncated) */}
|
{/* Detail label (truncated) */}
|
||||||
{detail && detail !== typeName && (
|
{detail && detail !== typeName && (
|
||||||
<text x={32} y={h / 2 + 14} fill="#5C5347" fontSize={10}>
|
<text x={32} y={h / 2 + 14} fill={isFailed ? '#C0392B' : '#5C5347'} fontSize={10}>
|
||||||
{detail.length > 22 ? detail.slice(0, 20) + '...' : detail}
|
{detail.length > 22 ? detail.slice(0, 20) + '...' : detail}
|
||||||
</text>
|
</text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Config badges */}
|
{/* Config badges */}
|
||||||
{config && <ConfigBadge nodeWidth={w} config={config} />}
|
{config && <ConfigBadge nodeWidth={w} config={config} />}
|
||||||
|
|
||||||
|
{/* Execution overlay: status badge at top-right */}
|
||||||
|
{isCompleted && (
|
||||||
|
<>
|
||||||
|
<circle cx={w - 8} cy={-8} r={8} fill="#3D7C47" />
|
||||||
|
<text
|
||||||
|
x={w - 8}
|
||||||
|
y={-4}
|
||||||
|
textAnchor="middle"
|
||||||
|
fill="white"
|
||||||
|
fontSize={11}
|
||||||
|
fontWeight={700}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isFailed && (
|
||||||
|
<>
|
||||||
|
<circle cx={w - 8} cy={-8} r={8} fill="#C0392B" />
|
||||||
|
<text
|
||||||
|
x={w - 8}
|
||||||
|
y={-4}
|
||||||
|
textAnchor="middle"
|
||||||
|
fill="white"
|
||||||
|
fontSize={12}
|
||||||
|
fontWeight={700}
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Execution overlay: duration text at bottom-right */}
|
||||||
|
{executionState && statusColor && (
|
||||||
|
<text
|
||||||
|
x={w - 6}
|
||||||
|
y={h - 4}
|
||||||
|
textAnchor="end"
|
||||||
|
fill={statusColor}
|
||||||
|
fontSize={9}
|
||||||
|
fontWeight={500}
|
||||||
|
>
|
||||||
|
{formatDuration(executionState.durationMs)}
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sub-route failure: drill-down arrow at bottom-left */}
|
||||||
|
{isFailed && executionState?.subRouteFailed && (
|
||||||
|
<text
|
||||||
|
x={6}
|
||||||
|
y={h - 4}
|
||||||
|
fill="#C0392B"
|
||||||
|
fontSize={11}
|
||||||
|
fontWeight={700}
|
||||||
|
>
|
||||||
|
↳
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user