Files
cameleer-server/ui/src/components/ProcessDiagram/DiagramNode.tsx
hsiegeln 3d5d462de0 fix: ENDPOINT node execution state, badge position, and edge traversal
- Synthesize COMPLETED state for ENDPOINT nodes when overlay is active
  (endpoints are route entry points, not in the processor execution tree)
- Move status badge (check/error) inside the card (top-right, below top bar)
  to avoid collision with ConfigBadge (TRACE/TAP) badges
- Include ENDPOINT nodes in edge traversal check so the edge from
  endpoint to first processor renders as green/traversed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:29:30 +01:00

193 lines
5.3 KiB
TypeScript

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';
const TOP_BAR_HEIGHT = 6;
const CORNER_RADIUS = 4;
interface DiagramNodeProps {
node: DiagramNodeType;
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,
executionState, overlayActive,
onClick, onDoubleClick, onMouseEnter, onMouseLeave,
}: DiagramNodeProps) {
const x = node.x ?? 0;
const y = node.y ?? 0;
const w = node.width ?? 120;
const h = node.height ?? 40;
const color = colorForType(node.type);
const icon = iconForType(node.type);
// Extract label parts: type name and detail
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 (
<g
data-node-id={node.id}
transform={`translate(${x}, ${y})`}
onClick={(e) => { e.stopPropagation(); onClick(); }}
onDoubleClick={(e) => { e.stopPropagation(); onDoubleClick?.(); }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={{ cursor: 'pointer' }}
opacity={isSkipped ? 0.35 : undefined}
>
{/* Selection ring */}
{isSelected && (
<rect
x={-2}
y={-2}
width={w + 4}
height={h + 4}
rx={CORNER_RADIUS + 2}
fill="none"
stroke="#C6820E"
strokeWidth={2.5}
/>
)}
{/* Card background */}
<rect
x={0}
y={0}
width={w}
height={h}
rx={CORNER_RADIUS}
fill={cardFill}
stroke={borderStroke}
strokeWidth={borderWidth}
/>
{/* Colored top bar */}
<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={topBarColor} />
{/* Icon */}
<text x={14} y={h / 2 + 6} fill={statusColor ?? color} fontSize={14}>
{icon}
</text>
{/* Type name */}
<text x={32} y={h / 2 + 1} fill={labelColor} fontSize={11} fontWeight={600}>
{typeName}
</text>
{/* Detail label (truncated) */}
{detail && detail !== typeName && (
<text x={32} y={h / 2 + 14} fill={isFailed ? '#C0392B' : '#5C5347'} fontSize={10}>
{detail.length > 22 ? detail.slice(0, 20) + '...' : detail}
</text>
)}
{/* Config badges */}
{config && <ConfigBadge nodeWidth={w} config={config} />}
{/* Execution overlay: status badge inside card, top-right corner */}
{isCompleted && (
<>
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="#3D7C47" />
<text
x={w - 10}
y={TOP_BAR_HEIGHT + 11}
textAnchor="middle"
fill="white"
fontSize={9}
fontWeight={700}
>
&#x2713;
</text>
</>
)}
{isFailed && (
<>
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="#C0392B" />
<text
x={w - 10}
y={TOP_BAR_HEIGHT + 11}
textAnchor="middle"
fill="white"
fontSize={9}
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}
>
&#x21B3;
</text>
)}
</g>
);
}