mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(ui): add Risk Pipeline View with Sankey chart to Overview page (#9320)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
@@ -1,10 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts";
|
||||
|
||||
import {
|
||||
AWSProviderBadge,
|
||||
AzureProviderBadge,
|
||||
GCPProviderBadge,
|
||||
GitHubProviderBadge,
|
||||
IacProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
import { ChartTooltip } from "./shared/chart-tooltip";
|
||||
|
||||
// Map node names to their corresponding provider icon components
|
||||
const PROVIDER_ICONS: Record<string, React.FC<IconSvgProps>> = {
|
||||
AWS: AWSProviderBadge,
|
||||
Azure: AzureProviderBadge,
|
||||
"Google Cloud": GCPProviderBadge,
|
||||
Kubernetes: KS8ProviderBadge,
|
||||
"Microsoft 365": M365ProviderBadge,
|
||||
GitHub: GitHubProviderBadge,
|
||||
"Infrastructure as Code": IacProviderBadge,
|
||||
"Oracle Cloud Infrastructure": OracleCloudProviderBadge,
|
||||
};
|
||||
|
||||
interface SankeyNode {
|
||||
name: string;
|
||||
newFindings?: number;
|
||||
@@ -47,14 +72,33 @@ interface NodeTooltipState {
|
||||
}
|
||||
|
||||
const TOOLTIP_OFFSET_PX = 10;
|
||||
const MIN_LINK_WIDTH = 4;
|
||||
|
||||
// Map severity node names to their filter values for the findings page
|
||||
const SEVERITY_FILTER_MAP: Record<string, string> = {
|
||||
Critical: "critical",
|
||||
High: "high",
|
||||
Medium: "medium",
|
||||
Low: "low",
|
||||
Informational: "informational",
|
||||
};
|
||||
|
||||
// Map color names to CSS variable names defined in globals.css
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
// Status colors
|
||||
Success: "--color-bg-pass",
|
||||
Pass: "--color-bg-pass",
|
||||
Fail: "--color-bg-fail",
|
||||
// Provider colors
|
||||
AWS: "--color-bg-data-aws",
|
||||
Azure: "--color-bg-data-azure",
|
||||
"Google Cloud": "--color-bg-data-gcp",
|
||||
Kubernetes: "--color-bg-data-kubernetes",
|
||||
"Microsoft 365": "--color-bg-data-m365",
|
||||
GitHub: "--color-bg-data-github",
|
||||
"Infrastructure as Code": "--color-bg-data-muted",
|
||||
"Oracle Cloud Infrastructure": "--color-bg-data-muted",
|
||||
// Severity colors
|
||||
Critical: "--color-bg-data-critical",
|
||||
High: "--color-bg-data-high",
|
||||
Medium: "--color-bg-data-medium",
|
||||
@@ -130,6 +174,7 @@ interface CustomNodeProps {
|
||||
onNodeHover?: (data: Omit<NodeTooltipState, "show">) => void;
|
||||
onNodeMove?: (position: { x: number; y: number }) => void;
|
||||
onNodeLeave?: () => void;
|
||||
onNodeClick?: (nodeName: string) => void;
|
||||
}
|
||||
|
||||
interface CustomLinkProps {
|
||||
@@ -184,12 +229,14 @@ const CustomNode = ({
|
||||
onNodeHover,
|
||||
onNodeMove,
|
||||
onNodeLeave,
|
||||
onNodeClick,
|
||||
}: CustomNodeProps) => {
|
||||
const isOut = x + width + 6 > containerWidth;
|
||||
const nodeName = payload.name;
|
||||
const color = colors[nodeName] || "var(--color-text-neutral-tertiary)";
|
||||
const isHidden = nodeName === "";
|
||||
const hasTooltip = !isHidden && payload.newFindings;
|
||||
const isClickable = SEVERITY_FILTER_MAP[nodeName] !== undefined;
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent) => {
|
||||
if (!hasTooltip) return;
|
||||
@@ -227,12 +274,30 @@ const CustomNode = ({
|
||||
onNodeLeave?.();
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (isClickable) {
|
||||
onNodeClick?.(nodeName);
|
||||
}
|
||||
};
|
||||
|
||||
const IconComponent = PROVIDER_ICONS[nodeName];
|
||||
const hasIcon = IconComponent !== undefined;
|
||||
const iconSize = 24;
|
||||
const iconGap = 8;
|
||||
|
||||
// Calculate text position accounting for icon
|
||||
const textOffsetX = isOut ? x - 6 : x + width + 6;
|
||||
const iconOffsetX = isOut
|
||||
? textOffsetX - iconSize - iconGap
|
||||
: textOffsetX + iconGap;
|
||||
|
||||
return (
|
||||
<g
|
||||
style={{ cursor: hasTooltip ? "pointer" : "default" }}
|
||||
style={{ cursor: isClickable || hasTooltip ? "pointer" : "default" }}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Rectangle
|
||||
x={x}
|
||||
@@ -244,9 +309,27 @@ const CustomNode = ({
|
||||
/>
|
||||
{!isHidden && (
|
||||
<>
|
||||
{hasIcon && (
|
||||
<foreignObject
|
||||
x={isOut ? iconOffsetX : textOffsetX}
|
||||
y={y + height / 2 - iconSize / 2 - 2}
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<IconComponent width={iconSize} height={iconSize} />
|
||||
</div>
|
||||
</foreignObject>
|
||||
)}
|
||||
<text
|
||||
textAnchor={isOut ? "end" : "start"}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
x={
|
||||
hasIcon
|
||||
? isOut
|
||||
? iconOffsetX - iconGap
|
||||
: textOffsetX + iconSize + iconGap * 2
|
||||
: textOffsetX
|
||||
}
|
||||
y={y + height / 2}
|
||||
fontSize="14"
|
||||
fill="var(--color-text-neutral-primary)"
|
||||
@@ -255,7 +338,13 @@ const CustomNode = ({
|
||||
</text>
|
||||
<text
|
||||
textAnchor={isOut ? "end" : "start"}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
x={
|
||||
hasIcon
|
||||
? isOut
|
||||
? iconOffsetX - iconGap
|
||||
: textOffsetX + iconSize + iconGap * 2
|
||||
: textOffsetX
|
||||
}
|
||||
y={y + height / 2 + 13}
|
||||
fontSize="12"
|
||||
fill="var(--color-text-neutral-secondary)"
|
||||
@@ -293,15 +382,18 @@ const CustomLink = ({
|
||||
const isHovered = hoveredLink !== null && hoveredLink === index;
|
||||
const hasHoveredLink = hoveredLink !== null;
|
||||
|
||||
// Ensure minimum link width for better visibility of small values
|
||||
const effectiveLinkWidth = Math.max(linkWidth, MIN_LINK_WIDTH);
|
||||
|
||||
const pathD = `
|
||||
M${sourceX},${sourceY + linkWidth / 2}
|
||||
C${sourceControlX},${sourceY + linkWidth / 2}
|
||||
${targetControlX},${targetY + linkWidth / 2}
|
||||
${targetX},${targetY + linkWidth / 2}
|
||||
L${targetX},${targetY - linkWidth / 2}
|
||||
C${targetControlX},${targetY - linkWidth / 2}
|
||||
${sourceControlX},${sourceY - linkWidth / 2}
|
||||
${sourceX},${sourceY - linkWidth / 2}
|
||||
M${sourceX},${sourceY + effectiveLinkWidth / 2}
|
||||
C${sourceControlX},${sourceY + effectiveLinkWidth / 2}
|
||||
${targetControlX},${targetY + effectiveLinkWidth / 2}
|
||||
${targetX},${targetY + effectiveLinkWidth / 2}
|
||||
L${targetX},${targetY - effectiveLinkWidth / 2}
|
||||
C${targetControlX},${targetY - effectiveLinkWidth / 2}
|
||||
${sourceControlX},${sourceY - effectiveLinkWidth / 2}
|
||||
${sourceX},${sourceY - effectiveLinkWidth / 2}
|
||||
Z
|
||||
`;
|
||||
|
||||
@@ -360,6 +452,7 @@ const CustomLink = ({
|
||||
};
|
||||
|
||||
export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
const router = useRouter();
|
||||
const [hoveredLink, setHoveredLink] = useState<number | null>(null);
|
||||
const [colors, setColors] = useState<Record<string, string>>({});
|
||||
const [linkTooltip, setLinkTooltip] = useState<LinkTooltipState>({
|
||||
@@ -423,11 +516,18 @@ export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
setNodeTooltip((prev) => ({ ...prev, show: false }));
|
||||
};
|
||||
|
||||
const handleNodeClick = (nodeName: string) => {
|
||||
const severityFilter = SEVERITY_FILTER_MAP[nodeName];
|
||||
if (severityFilter) {
|
||||
router.push(`/findings?filter[severity]=${severityFilter}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Create callback references that wrap custom props and Recharts-injected props
|
||||
const wrappedCustomNode = (
|
||||
props: Omit<
|
||||
CustomNodeProps,
|
||||
"colors" | "onNodeHover" | "onNodeMove" | "onNodeLeave"
|
||||
"colors" | "onNodeHover" | "onNodeMove" | "onNodeLeave" | "onNodeClick"
|
||||
>,
|
||||
) => (
|
||||
<CustomNode
|
||||
@@ -436,6 +536,7 @@ export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
onNodeHover={handleNodeHover}
|
||||
onNodeMove={handleNodeMove}
|
||||
onNodeLeave={handleNodeLeave}
|
||||
onNodeClick={handleNodeClick}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user