From 1a2a2ea3cc7e29573dd505333552a06b7c4c9ad8 Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Mon, 19 Jan 2026 18:04:23 +0100 Subject: [PATCH] fix(ui): make attack paths graph edges theme-aware (#9821) --- .../_components/graph/attack-path-graph.tsx | 27 +++++++++++-------- .../_components/graph/graph-legend.tsx | 26 +++++++++++++----- .../query-builder/_lib/graph-colors.ts | 4 ++- .../(workflow)/query-builder/_lib/index.ts | 3 ++- ui/components/ui/sidebar/menu-item.tsx | 11 ++------ 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx index 67ed4c42c7..10842492e8 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx @@ -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(null); const [zoomLevel, setZoomLevel] = useState(1); + const { resolvedTheme } = useTheme(); const zoomBehaviorRef = useRef | 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 ( ( +const EdgeLine = ({ + dashed, + edgeColor, +}: { + dashed: boolean; + edgeColor: string; +}) => ( ( y1="10" x2="44" y2="10" - stroke={GRAPH_EDGE_COLOR} + stroke={edgeColor} strokeWidth={3} strokeLinecap="round" strokeDasharray={dashed ? "8,6" : undefined} /> {/* Arrow head */} - + ); @@ -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" > - + Resource Connection @@ -477,7 +491,7 @@ export const GraphLegend = ({ data }: GraphLegendProps) => { role="img" aria-label="Dashed line: Finding connection" > - + Finding Connection diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts index eb207535ad..b49b058850 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts @@ -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"; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts index bd7581751f..abecf7a3c2 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts @@ -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, diff --git a/ui/components/ui/sidebar/menu-item.tsx b/ui/components/ui/sidebar/menu-item.tsx index 2c94527643..45337b6bf3 100644 --- a/ui/components/ui/sidebar/menu-item.tsx +++ b/ui/components/ui/sidebar/menu-item.tsx @@ -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 > -
- +
+ {isOpen && (