"use client"; import { type NodeProps, Position } from "@xyflow/react"; import { HiddenHandles } from "@/components/attack-paths/graph/nodes/hidden-handles"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/shadcn/tooltip"; import { resolveNodeColors, resolveNodeVisual } from "@/lib/attack-paths"; import { FINDING_NODE_DIMENSIONS } from "@/lib/attack-paths/node-dimensions"; import { getNodeLabelDisplay } from "@/lib/attack-paths/node-label-lines"; import type { GraphNode } from "@/types/attack-paths"; interface FindingNodeData { graphNode: GraphNode; [key: string]: unknown; } const NODE_WIDTH = FINDING_NODE_DIMENSIONS.WIDTH; const NODE_HEIGHT = FINDING_NODE_DIMENSIONS.HEIGHT; const TITLE_MAX_CHARS = FINDING_NODE_DIMENSIONS.LABEL_MAX_CHARS; const TITLE_MAX_LINES = FINDING_NODE_DIMENSIONS.LABEL_MAX_LINES; const BADGE_SIZE = 44; const BADGE_RADIUS = BADGE_SIZE / 2; const BADGE_CENTER_X = NODE_WIDTH / 2; const BADGE_CENTER_Y = 26; const BADGE_LEFT_X = BADGE_CENTER_X - BADGE_RADIUS; const BADGE_RIGHT_X = BADGE_CENTER_X + BADGE_RADIUS; const ICON_SIZE = 28; const ICON_X = BADGE_CENTER_X - ICON_SIZE / 2; const ICON_Y = BADGE_CENTER_Y - ICON_SIZE / 2; const TEXT_X = BADGE_CENTER_X; const TITLE_Y = 66; const TITLE_LINE_HEIGHT = 13; const SEVERITY_Y = 118; const severityLabel = (severity: unknown): string | undefined => { if (!severity) return undefined; const rawSeverity = Array.isArray(severity) ? severity[0] : severity; return String(rawSeverity).toLowerCase(); }; const toFindingIconTestId = (severity: string | undefined): string => `attack-path-finding-icon-${severity ?? "unknown"}`; const toAccessibleSeverity = (severity: string | undefined): string => severity ? `${severity.charAt(0).toUpperCase()}${severity.slice(1)}` : "Unknown"; export const FindingNode = ({ data, selected }: NodeProps) => { const { graphNode } = data as FindingNodeData; const { fillColor, borderColor } = resolveNodeColors({ labels: graphNode.labels, properties: graphNode.properties, selected, }); const title = String( graphNode.properties?.check_title || graphNode.properties?.name || graphNode.properties?.id || "Finding", ); const displayTitle = getNodeLabelDisplay( title, TITLE_MAX_CHARS, TITLE_MAX_LINES, ); const visual = resolveNodeVisual(graphNode); const Icon = visual.Icon; const severity = severityLabel(graphNode.properties?.severity); const iconLabel = `${toAccessibleSeverity(severity)} finding risk icon`; const badgeStrokeWidth = selected ? 4 : 2.5; const glowRadius = selected ? 32 : 30; const glowOpacity = selected ? 0.34 : 0.28; const nodeSvg = ( {displayTitle.lines.map((line, index) => ( {line} ))} {severity && ( {severity} )} ); return ( <> {displayTitle.isTruncated ? ( {nodeSvg} {title} ) : ( nodeSvg )} ); };