From 27a81defec40e3f5e1fb6c04bf14da3809db8aaf Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Fri, 5 Dec 2025 13:28:05 +0100 Subject: [PATCH] refactor(ui): simplify Risk Plot - use ThreatScore directly, remove mocks KISS/DRY improvements: - Remove unnecessary convertToRiskScore - use ThreatScore (0-100) directly - Reuse RiskPlotPoint type from actions instead of duplicate ScatterPoint - Remove CustomLegend wrapper - inline ChartLegend usage - Remove mock data file (risk-plot-view.tsx) - Rename 'Risk Score' to 'Threat Score' for consistency - Update XAxis domain from 0-10 to 0-100 - Simplify handlers with functional updates --- .../overview/risk-plot/risk-plot.adapter.ts | 13 +- .../risk-plot/types/risk-plot.types.ts | 2 +- .../risk-plot/risk-plot-client.tsx | 209 +++++++----------- .../graphs-tabs/risk-plot/risk-plot-view.tsx | 205 ----------------- 4 files changed, 83 insertions(+), 346 deletions(-) delete mode 100644 ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-view.tsx diff --git a/ui/actions/overview/risk-plot/risk-plot.adapter.ts b/ui/actions/overview/risk-plot/risk-plot.adapter.ts index cea0603753..d2b27cc06e 100644 --- a/ui/actions/overview/risk-plot/risk-plot.adapter.ts +++ b/ui/actions/overview/risk-plot/risk-plot.adapter.ts @@ -6,17 +6,6 @@ import type { RiskPlotPoint, } from "./types/risk-plot.types"; -/** - * Converts ThreatScore (0-100) to Risk Score (0-10). - * Both scales use "higher = better" convention. - */ -function convertToRiskScore(overallScore: number | null): number { - if (overallScore === null) return 0; - // ThreatScore is 0-100, we need 0-10 - // Higher = better in both scales - return overallScore / 10; -} - /** * Calculates percentage with proper rounding. */ @@ -90,7 +79,7 @@ export function adaptToRiskPlotData( } points.push({ - x: convertToRiskScore(providerData.overallScore), + x: providerData.overallScore ?? 0, y: providerData.failedFindings, provider: providerDisplayName, name: providerData.providerName, diff --git a/ui/actions/overview/risk-plot/types/risk-plot.types.ts b/ui/actions/overview/risk-plot/types/risk-plot.types.ts index 4b18d5363c..e10d88fe3f 100644 --- a/ui/actions/overview/risk-plot/types/risk-plot.types.ts +++ b/ui/actions/overview/risk-plot/types/risk-plot.types.ts @@ -8,7 +8,7 @@ import type { BarDataPoint } from "@/components/graphs/types"; * Each point represents a provider/account with its risk metrics. */ export interface RiskPlotPoint { - /** Risk Score (0-10 scale, derived from ThreatScore 0-100) */ + /** ThreatScore (0-100 scale, higher = better) */ x: number; /** Total failed findings count */ y: number; diff --git a/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-client.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-client.tsx index 96b72078cb..d680bfc95c 100644 --- a/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-client.tsx +++ b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-client.tsx @@ -22,6 +22,7 @@ import { YAxis, } from "recharts"; +import type { RiskPlotPoint } from "@/actions/overview/risk-plot"; import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart"; import { AlertPill } from "@/components/graphs/shared/alert-pill"; import { ChartLegend } from "@/components/graphs/shared/chart-legend"; @@ -33,25 +34,23 @@ import type { BarDataPoint } from "@/components/graphs/types"; import { mapProviderFiltersForFindings } from "@/lib/provider-helpers"; import { SEVERITY_FILTER_MAP } from "@/types/severities"; -// Risk Score colors using Threat Score system (same as threat-score component) -// Risk Score is 0-10 where higher = better (same as ThreatScore 0-100) -const RISK_COLORS = { - DANGER: "var(--bg-fail-primary)", // High risk (0-3) - WARNING: "var(--bg-warning-primary)", // Moderate risk (3.1-6) - SUCCESS: "var(--bg-pass-primary)", // Low risk (6.1-10) +// Threat Score colors (0-100 scale, higher = better) +const THREAT_COLORS = { + DANGER: "var(--bg-fail-primary)", // 0-30 + WARNING: "var(--bg-warning-primary)", // 31-60 + SUCCESS: "var(--bg-pass-primary)", // 61-100 } as const; /** - * Get color based on Risk Score (0-10 scale, higher = better) - * Uses same color scheme as Threat Score component + * Get color based on ThreatScore (0-100 scale, higher = better) */ -function getRiskScoreColor(riskScore: number): string { - if (riskScore > 6) return RISK_COLORS.SUCCESS; - if (riskScore > 3) return RISK_COLORS.WARNING; - return RISK_COLORS.DANGER; +function getThreatScoreColor(score: number): string { + if (score > 60) return THREAT_COLORS.SUCCESS; + if (score > 30) return THREAT_COLORS.WARNING; + return THREAT_COLORS.DANGER; } -// Map display names to CSS variables from globals.css +// Provider colors from globals.css const PROVIDER_COLORS: Record = { AWS: "var(--bg-data-aws)", Azure: "var(--bg-data-azure)", @@ -59,76 +58,55 @@ const PROVIDER_COLORS: Record = { Kubernetes: "var(--bg-data-kubernetes)", "Microsoft 365": "var(--bg-data-m365)", GitHub: "var(--bg-data-github)", - // Fallback for providers without specific colors - "MongoDB Atlas": "var(--bg-data-azure)", // Using azure as fallback - "Infrastructure as Code": "var(--bg-data-kubernetes)", // Using kubernetes as fallback - "Oracle Cloud Infrastructure": "var(--bg-data-gcp)", // Using gcp as fallback + "MongoDB Atlas": "var(--bg-data-azure)", + "Infrastructure as Code": "var(--bg-data-kubernetes)", + "Oracle Cloud Infrastructure": "var(--bg-data-gcp)", }; -export interface ScatterPoint { - x: number; - y: number; - provider: string; - name: string; - providerId: string; - severityData?: BarDataPoint[]; -} - interface RiskPlotClientProps { - data: ScatterPoint[]; + data: RiskPlotPoint[]; } interface TooltipProps { active?: boolean; - payload?: Array<{ payload: ScatterPoint }>; + payload?: Array<{ payload: RiskPlotPoint }>; } // Props that Recharts passes to the shape component interface RechartsScatterDotProps { cx: number; cy: number; - payload: ScatterPoint; + payload: RiskPlotPoint; } // Extended props for our custom scatter dot component interface ScatterDotProps extends RechartsScatterDotProps { - selectedPoint: ScatterPoint | null; - onSelectPoint: (point: ScatterPoint) => void; - allData: ScatterPoint[]; + selectedPoint: RiskPlotPoint | null; + onSelectPoint: (point: RiskPlotPoint) => void; + allData: RiskPlotPoint[]; selectedProvider: string | null; } -interface LegendProps { - providers: string[]; - selectedProvider: string | null; - onProviderClick: (provider: string) => void; -} - const CustomTooltip = ({ active, payload }: TooltipProps) => { - if (active && payload && payload.length) { - const data = payload[0].payload; - const riskColor = getRiskScoreColor(data.x); - // Convert Risk Score (0-10) to percentage (0-100) for display - const riskScorePercentage = data.x * 10; + if (!active || !payload?.length) return null; - return ( -
-

- {data.name} -

-

- - {riskScorePercentage}% - {" "} - Risk Score -

-
- -
+ const { name, x, y } = payload[0].payload; + const scoreColor = getThreatScoreColor(x); + + return ( +
+

+ {name} +

+

+ {x}%{" "} + Threat Score +

+
+
- ); - } - return null; +
+ ); }; const CustomScatterDot = ({ @@ -142,26 +120,25 @@ const CustomScatterDot = ({ }: ScatterDotProps) => { const isSelected = selectedPoint?.name === payload.name; const size = isSelected ? 18 : 8; - const selectedColor = "var(--bg-button-primary)"; // emerald-400 + const selectedColor = "var(--bg-button-primary)"; const fill = isSelected ? selectedColor : PROVIDER_COLORS[payload.provider] || "var(--color-text-neutral-tertiary)"; - - // Check if this point should be faded (when a provider is selected in legend) const isFaded = selectedProvider !== null && payload.provider !== selectedProvider; - const opacity = isFaded ? 0.2 : 1; const handleClick = () => { - const fullDataItem = allData?.find( - (d: ScatterPoint) => d.name === payload.name, - ); + const fullDataItem = allData?.find((d) => d.name === payload.name); onSelectPoint?.(fullDataItem || payload); }; return ( {isSelected && ( @@ -198,27 +175,6 @@ const CustomScatterDot = ({ ); }; -const CustomLegend = ({ - providers, - selectedProvider, - onProviderClick, -}: LegendProps) => { - // Build legend items from actual providers with correct colors - const items = providers.map((provider) => ({ - label: provider, - color: PROVIDER_COLORS[provider] || "var(--color-text-neutral-tertiary)", - dataKey: provider, - })); - - return ( - - ); -}; - /** * Factory function that creates a scatter dot shape component with closure over selection state. * Recharts shape prop types the callback parameter as `unknown` due to its flexible API. @@ -226,23 +182,20 @@ const CustomLegend = ({ * @see https://recharts.org/en-US/api/Scatter#shape */ function createScatterDotShape( - selectedPoint: ScatterPoint | null, - onSelectPoint: (point: ScatterPoint) => void, - allData: ScatterPoint[], + selectedPoint: RiskPlotPoint | null, + onSelectPoint: (point: RiskPlotPoint) => void, + allData: RiskPlotPoint[], selectedProvider: string | null, ): (props: unknown) => React.JSX.Element { - const ScatterDotShape = (props: unknown) => { - const rechartsProps = props as RechartsScatterDotProps; - return ( - - ); - }; + const ScatterDotShape = (props: unknown) => ( + + ); ScatterDotShape.displayName = "ScatterDotShape"; return ScatterDotShape; } @@ -250,34 +203,29 @@ function createScatterDotShape( export function RiskPlotClient({ data }: RiskPlotClientProps) { const router = useRouter(); const searchParams = useSearchParams(); - const [selectedPoint, setSelectedPoint] = useState(null); + const [selectedPoint, setSelectedPoint] = useState( + null, + ); const [selectedProvider, setSelectedProvider] = useState(null); - const dataByProvider = data.reduce( + // Group data by provider for separate Scatter series + const dataByProvider = data.reduce>( (acc, point) => { - const provider = point.provider; - if (!acc[provider]) { - acc[provider] = []; - } - acc[provider].push(point); + (acc[point.provider] ??= []).push(point); return acc; }, - {} as Record, + {}, ); - // Get unique providers for legend const providers = Object.keys(dataByProvider); - const handleSelectPoint = (point: ScatterPoint) => { - if (selectedPoint?.name === point.name) { - setSelectedPoint(null); - } else { - setSelectedPoint(point); - } + const handleSelectPoint = (point: RiskPlotPoint) => { + setSelectedPoint((current) => + current?.name === point.name ? null : point, + ); }; const handleProviderClick = (provider: string) => { - // Toggle selection: if already selected, deselect; otherwise select setSelectedProvider((current) => (current === provider ? null : provider)); }; @@ -335,16 +283,16 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) { Click to filter by provider.

- ({ + label: p, + color: + PROVIDER_COLORS[p] || "var(--color-text-neutral-tertiary)", + dataKey: p, + }))} + selectedItem={selectedProvider} + onItemClick={handleProviderClick} />
@@ -410,7 +363,7 @@ export function RiskPlotClient({ data }: RiskPlotClientProps) { {selectedPoint.name}

- Risk Score: {selectedPoint.x} | Failed Findings:{" "} + Threat Score: {selectedPoint.x}% | Failed Findings:{" "} {selectedPoint.y}

diff --git a/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-view.tsx b/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-view.tsx deleted file mode 100644 index 7c255da7d3..0000000000 --- a/ui/app/(prowler)/_new-overview/graphs-tabs/risk-plot/risk-plot-view.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import { RiskPlotClient, type ScatterPoint } from "./risk-plot-client"; - -// Mock data - Risk Score (0-10) vs Failed Findings count -const mockScatterData: ScatterPoint[] = [ - { - x: 9.2, - y: 1456, - provider: "AWS", - name: "Amazon RDS", - providerId: "mock-aws-1", - severityData: [ - { name: "Critical", value: 456 }, - { name: "High", value: 600 }, - { name: "Medium", value: 250 }, - { name: "Low", value: 120 }, - { name: "Info", value: 30 }, - ], - }, - { - x: 8.5, - y: 892, - provider: "AWS", - name: "Amazon EC2", - providerId: "mock-aws-2", - severityData: [ - { name: "Critical", value: 280 }, - { name: "High", value: 350 }, - { name: "Medium", value: 180 }, - { name: "Low", value: 70 }, - { name: "Info", value: 12 }, - ], - }, - { - x: 7.1, - y: 445, - provider: "AWS", - name: "Amazon S3", - providerId: "mock-aws-3", - severityData: [ - { name: "Critical", value: 140 }, - { name: "High", value: 180 }, - { name: "Medium", value: 90 }, - { name: "Low", value: 30 }, - { name: "Info", value: 5 }, - ], - }, - { - x: 6.3, - y: 678, - provider: "AWS", - name: "AWS Lambda", - providerId: "mock-aws-4", - severityData: [ - { name: "Critical", value: 214 }, - { name: "High", value: 270 }, - { name: "Medium", value: 135 }, - { name: "Low", value: 54 }, - { name: "Info", value: 5 }, - ], - }, - { - x: 4.2, - y: 156, - provider: "AWS", - name: "AWS Backup", - providerId: "mock-aws-5", - severityData: [ - { name: "Critical", value: 49 }, - { name: "High", value: 62 }, - { name: "Medium", value: 31 }, - { name: "Low", value: 12 }, - { name: "Info", value: 2 }, - ], - }, - { - x: 8.8, - y: 1023, - provider: "Azure", - name: "Azure SQL Database", - providerId: "mock-azure-1", - severityData: [ - { name: "Critical", value: 323 }, - { name: "High", value: 410 }, - { name: "Medium", value: 205 }, - { name: "Low", value: 82 }, - { name: "Info", value: 3 }, - ], - }, - { - x: 7.9, - y: 834, - provider: "Azure", - name: "Azure Virtual Machines", - providerId: "mock-azure-2", - severityData: [ - { name: "Critical", value: 263 }, - { name: "High", value: 334 }, - { name: "Medium", value: 167 }, - { name: "Low", value: 67 }, - { name: "Info", value: 3 }, - ], - }, - { - x: 6.4, - y: 567, - provider: "Azure", - name: "Azure Storage", - providerId: "mock-azure-3", - severityData: [ - { name: "Critical", value: 179 }, - { name: "High", value: 227 }, - { name: "Medium", value: 113 }, - { name: "Low", value: 45 }, - { name: "Info", value: 3 }, - ], - }, - { - x: 5.1, - y: 289, - provider: "Azure", - name: "Azure Key Vault", - providerId: "mock-azure-4", - severityData: [ - { name: "Critical", value: 91 }, - { name: "High", value: 115 }, - { name: "Medium", value: 58 }, - { name: "Low", value: 23 }, - { name: "Info", value: 2 }, - ], - }, - { - x: 7.6, - y: 712, - provider: "Google", - name: "Cloud SQL", - providerId: "mock-gcp-1", - severityData: [ - { name: "Critical", value: 225 }, - { name: "High", value: 285 }, - { name: "Medium", value: 142 }, - { name: "Low", value: 57 }, - { name: "Info", value: 3 }, - ], - }, - { - x: 6.9, - y: 623, - provider: "Google", - name: "Compute Engine", - providerId: "mock-gcp-2", - severityData: [ - { name: "Critical", value: 197 }, - { name: "High", value: 249 }, - { name: "Medium", value: 124 }, - { name: "Low", value: 50 }, - { name: "Info", value: 3 }, - ], - }, - { - x: 5.8, - y: 412, - provider: "Google", - name: "Cloud Storage", - providerId: "mock-gcp-3", - severityData: [ - { name: "Critical", value: 130 }, - { name: "High", value: 165 }, - { name: "Medium", value: 82 }, - { name: "Low", value: 33 }, - { name: "Info", value: 2 }, - ], - }, - { - x: 4.5, - y: 198, - provider: "Google", - name: "Cloud Run", - providerId: "mock-gcp-4", - severityData: [ - { name: "Critical", value: 63 }, - { name: "High", value: 79 }, - { name: "Medium", value: 39 }, - { name: "Low", value: 16 }, - { name: "Info", value: 1 }, - ], - }, - { - x: 8.9, - y: 945, - provider: "AWS", - name: "Amazon RDS Aurora", - providerId: "mock-aws-6", - severityData: [ - { name: "Critical", value: 299 }, - { name: "High", value: 378 }, - { name: "Medium", value: 189 }, - { name: "Low", value: 76 }, - { name: "Info", value: 3 }, - ], - }, -]; - -export function RiskPlotView() { - return ; -}