fix(ui): make attack paths graph edges theme-aware (#9821)

This commit is contained in:
Alan Buscaglia
2026-01-19 18:04:23 +01:00
committed by GitHub
parent e61d1401b9
commit 1a2a2ea3cc
5 changed files with 43 additions and 28 deletions

View File

@@ -3,6 +3,7 @@
import type { D3ZoomEvent, ZoomBehavior } from "d3";
import { select, zoom, zoomIdentity } from "d3";
import dagre from "dagre";
import { useTheme } from "next-themes";
import {
forwardRef,
type Ref,
@@ -20,7 +21,8 @@ import {
getNodeColor,
getPathEdges,
GRAPH_ALERT_BORDER_COLOR,
GRAPH_EDGE_COLOR,
GRAPH_EDGE_COLOR_DARK,
GRAPH_EDGE_COLOR_LIGHT,
GRAPH_EDGE_HIGHLIGHT_COLOR,
} from "../../_lib";
@@ -62,6 +64,7 @@ const AttackPathGraphComponent = forwardRef<
>(({ data, onNodeClick, selectedNodeId, isFilteredView = false }, ref) => {
const svgRef = useRef<SVGSVGElement>(null);
const [zoomLevel, setZoomLevel] = useState(1);
const { resolvedTheme } = useTheme();
const zoomBehaviorRef = useRef<ZoomBehavior<SVGSVGElement, unknown> | null>(
null,
);
@@ -88,6 +91,10 @@ const AttackPathGraphComponent = forwardRef<
}>
>([]);
// Get edge color based on current theme
const edgeColor =
resolvedTheme === "dark" ? GRAPH_EDGE_COLOR_DARK : GRAPH_EDGE_COLOR_LIGHT;
// Keep selectedNodeIdRef in sync with selectedNodeId
useEffect(() => {
selectedNodeIdRef.current = selectedNodeId ?? null;
@@ -160,17 +167,14 @@ const AttackPathGraphComponent = forwardRef<
const edgeId = `${edgeData.sourceId}-${edgeData.targetId}`;
const isInPath = pathEdges.has(edgeId);
select(this)
.attr(
"stroke",
isInPath ? GRAPH_EDGE_HIGHLIGHT_COLOR : GRAPH_EDGE_COLOR,
)
.attr("stroke", isInPath ? GRAPH_EDGE_HIGHLIGHT_COLOR : edgeColor)
.attr(
"marker-end",
isInPath ? "url(#arrowhead-highlight)" : "url(#arrowhead)",
);
});
}
}, [selectedNodeId]);
}, [selectedNodeId, edgeColor]);
useImperativeHandle(ref, () => ({
zoomIn: () => {
@@ -406,7 +410,7 @@ const AttackPathGraphComponent = forwardRef<
.attr("flood-color", GRAPH_EDGE_HIGHLIGHT_COLOR)
.attr("flood-opacity", "0.8");
// Arrow marker (default white) - refX=10 places the arrow tip exactly at the line endpoint
// Arrow marker (theme-aware) - refX=10 places the arrow tip exactly at the line endpoint
defs
.append("marker")
.attr("id", "arrowhead")
@@ -418,7 +422,7 @@ const AttackPathGraphComponent = forwardRef<
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 z")
.attr("fill", GRAPH_EDGE_COLOR);
.attr("fill", edgeColor);
// Arrow marker (highlighted orange) for hover state
defs
@@ -541,7 +545,7 @@ const AttackPathGraphComponent = forwardRef<
"y2",
(d) => getEdgePoints(d.sourceId, d.targetId, d.source, d.target).y2,
)
.attr("stroke", GRAPH_EDGE_COLOR)
.attr("stroke", edgeColor)
.attr("stroke-width", 3)
.attr("stroke-linecap", "round")
.attr("stroke-dasharray", (d) => {
@@ -640,7 +644,7 @@ const AttackPathGraphComponent = forwardRef<
.attr("marker-end", "url(#arrowhead-highlight)");
} else {
select(this)
.attr("stroke", GRAPH_EDGE_COLOR)
.attr("stroke", edgeColor)
.attr("marker-end", "url(#arrowhead)");
}
});
@@ -1152,8 +1156,9 @@ const AttackPathGraphComponent = forwardRef<
// D3's imperative rendering model requires controlled re-renders.
// We intentionally only re-render on data/view changes, not on callback refs
// (onNodeClick, selectedNodeId) which would cause unnecessary D3 re-renders.
// edgeColor is included to re-render when theme changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data, isFilteredView]);
}, [data, isFilteredView, edgeColor]);
return (
<svg

View File

@@ -1,5 +1,7 @@
"use client";
import { useTheme } from "next-themes";
import { Card, CardContent } from "@/components/shadcn";
import {
Tooltip,
@@ -12,7 +14,8 @@ import type { AttackPathGraphData } from "@/types/attack-paths";
import {
getNodeBorderColor,
getNodeColor,
GRAPH_EDGE_COLOR,
GRAPH_EDGE_COLOR_DARK,
GRAPH_EDGE_COLOR_LIGHT,
GRAPH_NODE_BORDER_COLORS,
GRAPH_NODE_COLORS,
} from "../../_lib/graph-colors";
@@ -361,7 +364,13 @@ const GlobeShape = ({
/**
* Edge line component for legend
*/
const EdgeLine = ({ dashed }: { dashed: boolean }) => (
const EdgeLine = ({
dashed,
edgeColor,
}: {
dashed: boolean;
edgeColor: string;
}) => (
<svg
width="60"
height="20"
@@ -375,13 +384,13 @@ const EdgeLine = ({ dashed }: { dashed: boolean }) => (
y1="10"
x2="44"
y2="10"
stroke={GRAPH_EDGE_COLOR}
stroke={edgeColor}
strokeWidth={3}
strokeLinecap="round"
strokeDasharray={dashed ? "8,6" : undefined}
/>
{/* Arrow head */}
<polygon points="44,5 56,10 44,15" fill={GRAPH_EDGE_COLOR} />
<polygon points="44,5 56,10 44,15" fill={edgeColor} />
</svg>
);
@@ -393,8 +402,13 @@ interface GraphLegendProps {
* Legend for attack path graph node types and edge styles
*/
export const GraphLegend = ({ data }: GraphLegendProps) => {
const { resolvedTheme } = useTheme();
const nodeTypes = extractNodeTypes(data?.nodes);
// Get edge color based on current theme
const edgeColor =
resolvedTheme === "dark" ? GRAPH_EDGE_COLOR_DARK : GRAPH_EDGE_COLOR_LIGHT;
// Check if there are any findings in the data
const hasFindings = nodeTypes.some((type) =>
type.toLowerCase().includes("finding"),
@@ -458,7 +472,7 @@ export const GraphLegend = ({ data }: GraphLegendProps) => {
role="img"
aria-label="Solid line: Resource connection"
>
<EdgeLine dashed={false} />
<EdgeLine dashed={false} edgeColor={edgeColor} />
<span className="text-text-neutral-secondary text-xs">
Resource Connection
</span>
@@ -477,7 +491,7 @@ export const GraphLegend = ({ data }: GraphLegendProps) => {
role="img"
aria-label="Dashed line: Finding connection"
>
<EdgeLine dashed={true} />
<EdgeLine dashed={true} edgeColor={edgeColor} />
<span className="text-text-neutral-secondary text-xs">
Finding Connection
</span>

View File

@@ -51,7 +51,9 @@ export const GRAPH_NODE_BORDER_COLORS = {
default: "#22d3ee",
} as const;
export const GRAPH_EDGE_COLOR = "#ffffff"; // White (default)
// Edge colors per theme
export const GRAPH_EDGE_COLOR_DARK = "#ffffff"; // White for dark theme
export const GRAPH_EDGE_COLOR_LIGHT = "#1e293b"; // Slate 800 for light theme
export const GRAPH_EDGE_HIGHLIGHT_COLOR = "#f97316"; // Orange 500 (on hover)
export const GRAPH_EDGE_GLOW_COLOR = "#fb923c";
export const GRAPH_SELECTION_COLOR = "#ffffff";

View File

@@ -8,7 +8,8 @@ export {
getNodeBorderColor,
getNodeColor,
GRAPH_ALERT_BORDER_COLOR,
GRAPH_EDGE_COLOR,
GRAPH_EDGE_COLOR_DARK,
GRAPH_EDGE_COLOR_LIGHT,
GRAPH_EDGE_HIGHLIGHT_COLOR,
GRAPH_NODE_BORDER_COLORS,
GRAPH_NODE_COLORS,

View File

@@ -46,19 +46,12 @@ export const MenuItem = ({
variant={isActive ? "menu-active" : "menu-inactive"}
className={cn(
isOpen ? "w-full justify-start" : "w-14 justify-center",
highlight &&
"relative overflow-hidden before:absolute before:inset-0 before:rounded-lg before:bg-gradient-to-r before:from-emerald-500/20 before:via-teal-400/20 before:to-emerald-300/20 before:opacity-70",
)}
asChild
>
<Link href={href} target={target}>
<div className="relative z-10 flex items-center">
<span
className={cn(
isOpen ? "mr-4" : "",
highlight && "text-button-primary",
)}
>
<div className="flex items-center">
<span className={cn(isOpen ? "mr-4" : "")}>
<Icon size={18} />
</span>
{isOpen && (