mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-25 04:44:20 +00:00
122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { type NodeProps } from "@xyflow/react";
|
|
import { Crosshair } from "lucide-react";
|
|
|
|
import { GraphNodeShell } from "@/components/attack-paths/graph/nodes/graph-node-shell";
|
|
import {
|
|
GRAPH_OUTCOME_BORDER_COLOR,
|
|
GRAPH_OUTCOME_FILL_COLOR,
|
|
} from "@/lib/attack-paths";
|
|
import { OUTCOME_PROPS } from "@/lib/attack-paths/group-graph";
|
|
import { OUTCOME_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 OutcomeNodeData {
|
|
graphNode: GraphNode;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
const NODE_WIDTH = OUTCOME_NODE_DIMENSIONS.WIDTH;
|
|
const NODE_HEIGHT = OUTCOME_NODE_DIMENSIONS.HEIGHT;
|
|
const NAME_MAX_CHARS = OUTCOME_NODE_DIMENSIONS.LABEL_MAX_CHARS;
|
|
const NAME_MAX_LINES = OUTCOME_NODE_DIMENSIONS.LABEL_MAX_LINES;
|
|
const BADGE_RADIUS = 24;
|
|
const BADGE_CENTER_X = NODE_WIDTH / 2;
|
|
const BADGE_CENTER_Y = 26;
|
|
const ICON_SIZE = 26;
|
|
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 KICKER_Y = 64;
|
|
const LABEL_Y = 78;
|
|
const LABEL_LINE_HEIGHT = 13;
|
|
|
|
export const OutcomeNode = ({ data, selected }: NodeProps) => {
|
|
const { graphNode } = data as OutcomeNodeData;
|
|
const label = String(graphNode.properties[OUTCOME_PROPS.LABEL] ?? "Outcome");
|
|
const partial = Boolean(graphNode.properties[OUTCOME_PROPS.PARTIAL]);
|
|
|
|
const name = getNodeLabelDisplay(label, NAME_MAX_CHARS, NAME_MAX_LINES);
|
|
|
|
return (
|
|
<GraphNodeShell
|
|
width={NODE_WIDTH}
|
|
height={NODE_HEIGHT}
|
|
badgeCenterX={BADGE_CENTER_X}
|
|
badgeCenterY={BADGE_CENTER_Y}
|
|
badgeRadius={BADGE_RADIUS}
|
|
testId="attack-path-outcome-node"
|
|
tooltip={name.isTruncated ? label : undefined}
|
|
>
|
|
<circle
|
|
cx={BADGE_CENTER_X}
|
|
cy={BADGE_CENTER_Y}
|
|
r={BADGE_RADIUS + 5}
|
|
fill={GRAPH_OUTCOME_BORDER_COLOR}
|
|
fillOpacity={0.22}
|
|
pointerEvents="none"
|
|
/>
|
|
<circle
|
|
cx={BADGE_CENTER_X}
|
|
cy={BADGE_CENTER_Y}
|
|
r={BADGE_RADIUS}
|
|
fill={GRAPH_OUTCOME_FILL_COLOR}
|
|
stroke={GRAPH_OUTCOME_BORDER_COLOR}
|
|
strokeWidth={selected ? 4 : 2}
|
|
// Partial/latent outcomes are drawn with a dashed ring as a first pass.
|
|
strokeDasharray={partial ? "4 3" : undefined}
|
|
/>
|
|
<g
|
|
aria-label="Outcome"
|
|
role="img"
|
|
transform={`translate(${ICON_X}, ${ICON_Y})`}
|
|
>
|
|
<Crosshair
|
|
aria-hidden="true"
|
|
color="#ffffff"
|
|
focusable="false"
|
|
height={ICON_SIZE}
|
|
role="presentation"
|
|
size={ICON_SIZE}
|
|
width={ICON_SIZE}
|
|
/>
|
|
</g>
|
|
<text
|
|
x={TEXT_X}
|
|
y={KICKER_Y}
|
|
textAnchor="middle"
|
|
fontSize="8px"
|
|
fontWeight="700"
|
|
letterSpacing="1"
|
|
fill={GRAPH_OUTCOME_BORDER_COLOR}
|
|
pointerEvents="none"
|
|
>
|
|
{partial ? "LATENT OUTCOME" : "OUTCOME"}
|
|
</text>
|
|
<text
|
|
x={TEXT_X}
|
|
y={LABEL_Y}
|
|
textAnchor="middle"
|
|
dominantBaseline="middle"
|
|
fill="#ffffff"
|
|
style={{ textShadow: "0 1px 2px rgba(0,0,0,0.5)" }}
|
|
pointerEvents="none"
|
|
>
|
|
{name.lines.map((line, index) => (
|
|
<tspan
|
|
key={`${line}-${index}`}
|
|
x={TEXT_X}
|
|
y={LABEL_Y + index * LABEL_LINE_HEIGHT}
|
|
fontSize="11px"
|
|
fontWeight="700"
|
|
>
|
|
{line}
|
|
</tspan>
|
|
))}
|
|
</text>
|
|
</GraphNodeShell>
|
|
);
|
|
};
|