import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; import type { NodeConfig, LatencyHeatmapEntry } from './types'; import type { NodeExecutionState } from '../ExecutionDiagram/types'; import { colorForType, iconForType, type IconElement } from './node-colors'; import { ConfigBadge } from './ConfigBadge'; const TOP_BAR_HEIGHT = 6; const TEXT_LEFT = 32; const TEXT_RIGHT_PAD = 24; const CORNER_RADIUS = 4; interface DiagramNodeProps { node: DiagramNodeType; isHovered: boolean; isSelected: boolean; config?: NodeConfig; executionState?: NodeExecutionState; overlayActive?: boolean; heatmapEntry?: LatencyHeatmapEntry; onClick: () => void; onDoubleClick?: () => void; onMouseEnter: () => void; onMouseLeave: () => void; } /** Interpolate green (120°) → yellow (60°) → red (0°) based on pctOfRoute */ function heatmapColor(pct: number): string { const clamped = Math.max(0, Math.min(100, pct)); // 0% → hue 120 (green), 50% → hue 60 (yellow), 100% → hue 0 (red) const hue = 120 - (clamped / 100) * 120; return `hsl(${Math.round(hue)}, 70%, 92%)`; } function heatmapBorderColor(pct: number): string { const clamped = Math.max(0, Math.min(100, pct)); const hue = 120 - (clamped / 100) * 120; return `hsl(${Math.round(hue)}, 60%, 50%)`; } 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, heatmapEntry, 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 iconElements = iconForType(node.type); // Extract label parts: type name and detail const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? ''; const detail = node.label || ''; const resolvedUri = executionState?.resolvedEndpointUri; // Overlay state derivation const isCompleted = executionState?.status === 'COMPLETED'; const isFailed = executionState?.status === 'FAILED'; const isSkipped = overlayActive && !executionState; // Colors based on execution state (heatmap takes priority when no execution overlay) 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'; } else if (heatmapEntry && !overlayActive) { cardFill = heatmapColor(heatmapEntry.pctOfRoute); borderStroke = heatmapBorderColor(heatmapEntry.pctOfRoute); borderWidth = 1.5; topBarColor = heatmapBorderColor(heatmapEntry.pctOfRoute); } const statusColor = isCompleted ? '#3D7C47' : isFailed ? '#C0392B' : undefined; return ( { e.stopPropagation(); onClick(); }} onDoubleClick={(e) => { e.stopPropagation(); onDoubleClick?.(); }} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} style={{ cursor: 'pointer' }} opacity={isSkipped ? 0.35 : undefined} > {/* Selection ring */} {isSelected && ( )} {/* Card background */} {/* Colored top bar */} {/* Clip path for text area */} {/* Icon (lucide 24×24 scaled to 14px) */} {iconElements.map((el: IconElement, i: number) => 'd' in el ? : )} {/* Type name + detail + resolved URI (clipped to available width) */} {resolvedUri ? ( <> {typeName} {detail && detail !== typeName && ( {detail} )} → {resolvedUri.split('?')[0]} ) : ( <> {typeName} {detail && detail !== typeName && ( {detail} )} )} {/* Config badges */} {(config || executionState?.hasTraceData) && ( )} {/* Execution overlay: status badge inside card, top-right corner */} {isCompleted && ( <> )} {isFailed && ( <> )} {/* Execution overlay: duration text at bottom-right */} {executionState && statusColor && ( {formatDuration(executionState.durationMs)} )} {/* Heatmap: avg duration label at bottom-right */} {heatmapEntry && !overlayActive && !executionState && ( {formatDuration(heatmapEntry.avgDurationMs)} )} {/* Sub-route failure: drill-down arrow at bottom-left */} {isFailed && executionState?.subRouteFailed && ( )} ); }