"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 { RESOURCE_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 ResourceNodeData { graphNode: GraphNode; hasFindings?: boolean; [key: string]: unknown; } const NODE_WIDTH = RESOURCE_NODE_DIMENSIONS.WIDTH; const NODE_HEIGHT = RESOURCE_NODE_DIMENSIONS.HEIGHT; const NAME_MAX_CHARS = RESOURCE_NODE_DIMENSIONS.LABEL_MAX_CHARS; const NAME_MAX_LINES = RESOURCE_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 NAME_Y = 66; const NAME_LINE_HEIGHT = 13; const TYPE_Y = 118; const toIconTestId = (description: string): string => `attack-path-node-icon-${description .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, "")}`; export const ResourceNode = ({ data, selected }: NodeProps) => { const { graphNode, hasFindings } = data as ResourceNodeData; const { fillColor, borderColor } = resolveNodeColors({ labels: graphNode.labels, properties: graphNode.properties, selected, hasFindings, }); const badgeStrokeWidth = selected ? 4 : hasFindings ? 3 : 1.5; const glowRadius = selected ? 31 : hasFindings ? 29 : 0; const glowOpacity = selected ? 0.32 : hasFindings ? 0.26 : 0; const visual = resolveNodeVisual(graphNode); const Icon = visual.Icon; const displayName = getNodeLabelDisplay( visual.displayName, NAME_MAX_CHARS, NAME_MAX_LINES, ); const typeLabel = visual.description; const iconLabel = `${visual.description} icon`; const nodeSvg = ( {glowRadius > 0 && ( )} {displayName.lines.map((line, index) => ( {line} ))} {typeLabel && ( {typeLabel} )} ); return ( <> {displayName.isTruncated ? ( {nodeSvg} {visual.displayName} ) : ( nodeSvg )} ); };