mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
fix(ui): make attack paths graph edges theme-aware (#9821)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user