"use client"; import { type NodeProps } from "@xyflow/react"; import { GraphNodeShell } from "@/components/attack-paths/graph/nodes/graph-node-shell"; import { GRAPH_COUNT_BADGE_STROKE_COLOR, resolveNodeColors, resolveNodeVisual, } from "@/lib/attack-paths"; import { GROUP_PROPS } from "@/lib/attack-paths/group-graph"; import { GROUP_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 GroupNodeData { graphNode: GraphNode; [key: string]: unknown; } const NODE_WIDTH = GROUP_NODE_DIMENSIONS.WIDTH; const NODE_HEIGHT = GROUP_NODE_DIMENSIONS.HEIGHT; const NAME_MAX_CHARS = GROUP_NODE_DIMENSIONS.LABEL_MAX_CHARS; const NAME_MAX_LINES = GROUP_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 ICON_SIZE = 28; const ICON_X = BADGE_CENTER_X - ICON_SIZE / 2; const ICON_Y = BADGE_CENTER_Y - ICON_SIZE / 2; // Count badge, top-right of the class icon. const COUNT_CX = BADGE_CENTER_X + BADGE_RADIUS - 2; const COUNT_CY = BADGE_CENTER_Y - BADGE_RADIUS + 2; const COUNT_R = 11; const TEXT_X = BADGE_CENTER_X; const NAME_Y = 62; const NAME_LINE_HEIGHT = 13; const COUNT_LABEL_Y = 96; const HINT_Y = 112; export const GroupNode = ({ data, selected }: NodeProps) => { const { graphNode } = data as GroupNodeData; const classLabel = String(graphNode.properties[GROUP_PROPS.CLASS] ?? ""); const className = String(graphNode.properties[GROUP_PROPS.CLASS_NAME] ?? ""); const count = Number(graphNode.properties[GROUP_PROPS.COUNT] ?? 0); // Members carry findings -> red cue, so the user knows to expand (matches the // per-resource "red = findings" convention). const hasFindings = Boolean(graphNode.properties[GROUP_PROPS.HAS_FINDINGS]); // Resolve the class icon/colors from a representative node of the class. const representative: GraphNode = { id: graphNode.id, labels: [classLabel], properties: {}, }; const visual = resolveNodeVisual(representative); const Icon = visual.Icon; const { fillColor, borderColor } = resolveNodeColors({ labels: [classLabel], properties: {}, selected, hasFindings, }); const fullName = className || visual.description; const name = getNodeLabelDisplay(fullName, NAME_MAX_CHARS, NAME_MAX_LINES); return ( {hasFindings && ( )} {/* count badge */} {count} {name.lines.map((line, index) => ( {line} ))} {count} {count === 1 ? "resource" : "resources"} click to expand ); };