mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-09 21:14:22 +00:00
feat(ui/overview): add click navigation for charts and threat score improvements (#9281)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { getFindingsByStatus } from "@/actions/overview/overview";
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { pickFilterParams } from "../../lib/filter-params";
|
||||
@@ -11,7 +12,10 @@ export const CheckFindingsSSR = async ({
|
||||
}) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
|
||||
const findingsByStatus = await getFindingsByStatus({ filters });
|
||||
const [findingsByStatus, providersData] = await Promise.all([
|
||||
getFindingsByStatus({ filters }),
|
||||
getProviders({ page: 1, pageSize: 200 }),
|
||||
]);
|
||||
|
||||
if (!findingsByStatus) {
|
||||
return (
|
||||
@@ -21,29 +25,21 @@ export const CheckFindingsSSR = async ({
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
fail = 0,
|
||||
pass = 0,
|
||||
muted_new = 0,
|
||||
muted_changed = 0,
|
||||
fail_new = 0,
|
||||
pass_new = 0,
|
||||
} = findingsByStatus?.data?.attributes || {};
|
||||
const attributes = findingsByStatus?.data?.attributes || {};
|
||||
|
||||
const mutedTotal = muted_new + muted_changed;
|
||||
const { fail = 0, pass = 0, fail_new = 0, pass_new = 0 } = attributes;
|
||||
|
||||
return (
|
||||
<StatusChart
|
||||
failFindingsData={{
|
||||
total: fail,
|
||||
new: fail_new,
|
||||
muted: mutedTotal,
|
||||
}}
|
||||
passFindingsData={{
|
||||
total: pass,
|
||||
new: pass_new,
|
||||
muted: mutedTotal,
|
||||
}}
|
||||
providers={providersData?.data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-1
@@ -1,4 +1,5 @@
|
||||
import { getFindingsBySeverity } from "@/actions/overview/overview";
|
||||
import { getProviders } from "@/actions/providers";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
import { pickFilterParams } from "../../lib/filter-params";
|
||||
@@ -11,7 +12,10 @@ export const RiskSeverityChartDetailSSR = async ({
|
||||
}) => {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
|
||||
const findingsBySeverity = await getFindingsBySeverity({ filters });
|
||||
const [findingsBySeverity, providersData] = await Promise.all([
|
||||
getFindingsBySeverity({ filters }),
|
||||
getProviders({ page: 1, pageSize: 200 }),
|
||||
]);
|
||||
|
||||
if (!findingsBySeverity) {
|
||||
return (
|
||||
@@ -36,6 +40,7 @@ export const RiskSeverityChartDetailSSR = async ({
|
||||
medium={medium}
|
||||
low={low}
|
||||
informational={informational}
|
||||
providers={providersData?.data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+62
-1
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart";
|
||||
import { BarDataPoint } from "@/components/graphs/types";
|
||||
import {
|
||||
@@ -11,12 +13,23 @@ import {
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
|
||||
interface ProviderAttributes {
|
||||
uid: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface Provider {
|
||||
id: string;
|
||||
attributes: ProviderAttributes;
|
||||
}
|
||||
|
||||
interface RiskSeverityChartProps {
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
informational: number;
|
||||
providers?: Provider[];
|
||||
}
|
||||
|
||||
export const RiskSeverityChart = ({
|
||||
@@ -25,7 +38,55 @@ export const RiskSeverityChart = ({
|
||||
medium,
|
||||
low,
|
||||
informational,
|
||||
providers = [],
|
||||
}: RiskSeverityChartProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const handleBarClick = (dataPoint: BarDataPoint) => {
|
||||
// Build the URL with current filters plus severity and muted
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// Convert filter[provider_id__in] to filter[provider_uid__in] for findings page
|
||||
const providerIds = params.get("filter[provider_id__in]");
|
||||
if (providerIds) {
|
||||
params.delete("filter[provider_id__in]");
|
||||
// Remove provider_type__in since provider_id__in is more specific
|
||||
params.delete("filter[provider_type__in]");
|
||||
|
||||
const ids = providerIds.split(",");
|
||||
const uids = ids
|
||||
.map((id) => {
|
||||
const provider = providers.find((p) => p.id === id);
|
||||
return provider?.attributes.uid;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (uids.length > 0) {
|
||||
params.set("filter[provider_uid__in]", uids.join(","));
|
||||
}
|
||||
}
|
||||
|
||||
// Map severity name to lowercase for the filter
|
||||
const severityMap: Record<string, string> = {
|
||||
Critical: "critical",
|
||||
High: "high",
|
||||
Medium: "medium",
|
||||
Low: "low",
|
||||
Info: "informational",
|
||||
};
|
||||
|
||||
const severity = severityMap[dataPoint.name];
|
||||
if (severity) {
|
||||
params.set("filter[severity__in]", severity);
|
||||
}
|
||||
|
||||
// Add exclude muted findings filter
|
||||
params.set("filter[muted]", "false");
|
||||
|
||||
// Navigate to findings page
|
||||
router.push(`/findings?${params.toString()}`);
|
||||
};
|
||||
// Calculate total findings
|
||||
const totalFindings = critical + high + medium + low + informational;
|
||||
|
||||
@@ -68,7 +129,7 @@ export const RiskSeverityChart = ({
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 items-center justify-start px-6">
|
||||
<HorizontalBarChart data={chartData} />
|
||||
<HorizontalBarChart data={chartData} onBarClick={handleBarClick} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Bell, BellOff, ShieldCheck, TriangleAlert } from "lucide-react";
|
||||
import { Bell, ShieldCheck, TriangleAlert } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { DonutChart } from "@/components/graphs/donut-chart";
|
||||
import { DonutDataPoint } from "@/components/graphs/types";
|
||||
@@ -15,26 +16,76 @@ import {
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
|
||||
interface FindingsData {
|
||||
total: number;
|
||||
new: number;
|
||||
}
|
||||
|
||||
interface ProviderAttributes {
|
||||
uid: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface Provider {
|
||||
id: string;
|
||||
attributes: ProviderAttributes;
|
||||
}
|
||||
|
||||
interface StatusChartProps {
|
||||
failFindingsData: {
|
||||
total: number;
|
||||
new: number;
|
||||
muted: number;
|
||||
};
|
||||
passFindingsData: {
|
||||
total: number;
|
||||
new: number;
|
||||
muted: number;
|
||||
};
|
||||
failFindingsData: FindingsData;
|
||||
passFindingsData: FindingsData;
|
||||
providers?: Provider[];
|
||||
}
|
||||
|
||||
export const StatusChart = ({
|
||||
failFindingsData,
|
||||
passFindingsData,
|
||||
providers = [],
|
||||
}: StatusChartProps) => {
|
||||
// Calculate total findings
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Calculate total from displayed findings (fail + pass)
|
||||
const totalFindings = failFindingsData.total + passFindingsData.total;
|
||||
|
||||
const handleSegmentClick = (dataPoint: DonutDataPoint) => {
|
||||
// Build the URL with current filters plus status and muted
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// Convert filter[provider_id__in] to filter[provider_uid__in] for findings page
|
||||
const providerIds = params.get("filter[provider_id__in]");
|
||||
if (providerIds) {
|
||||
params.delete("filter[provider_id__in]");
|
||||
// Remove provider_type__in since provider_id__in is more specific
|
||||
params.delete("filter[provider_type__in]");
|
||||
|
||||
const ids = providerIds.split(",");
|
||||
const uids = ids
|
||||
.map((id) => {
|
||||
const provider = providers.find((p) => p.id === id);
|
||||
return provider?.attributes.uid;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (uids.length > 0) {
|
||||
params.set("filter[provider_uid__in]", uids.join(","));
|
||||
}
|
||||
}
|
||||
|
||||
// Add status filter based on which segment was clicked
|
||||
if (dataPoint.name === "Fail Findings") {
|
||||
params.set("filter[status__in]", "FAIL");
|
||||
} else if (dataPoint.name === "Pass Findings") {
|
||||
params.set("filter[status__in]", "PASS");
|
||||
}
|
||||
|
||||
// Add exclude muted findings filter
|
||||
params.set("filter[muted]", "false");
|
||||
|
||||
// Navigate to findings page
|
||||
router.push(`/findings?${params.toString()}`);
|
||||
};
|
||||
|
||||
// Calculate percentages
|
||||
const failPercentage = calculatePercentage(
|
||||
failFindingsData.total,
|
||||
@@ -93,6 +144,7 @@ export const StatusChart = ({
|
||||
value: totalFindings.toLocaleString(),
|
||||
label: "Total Findings",
|
||||
}}
|
||||
onSegmentClick={handleSegmentClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -109,10 +161,7 @@ export const StatusChart = ({
|
||||
variant: CardVariant.fail,
|
||||
}}
|
||||
label="Fail Findings"
|
||||
stats={[
|
||||
{ icon: Bell, label: `${failFindingsData.new} New` },
|
||||
{ icon: BellOff, label: `${failFindingsData.muted} Muted` },
|
||||
]}
|
||||
stats={[{ icon: Bell, label: `${failFindingsData.new} New` }]}
|
||||
emptyState={
|
||||
failFindingsData.total === 0
|
||||
? { message: "No failed findings to display" }
|
||||
@@ -133,10 +182,7 @@ export const StatusChart = ({
|
||||
variant: CardVariant.pass,
|
||||
}}
|
||||
label="Pass Findings"
|
||||
stats={[
|
||||
{ icon: Bell, label: `${passFindingsData.new} New` },
|
||||
{ icon: BellOff, label: `${passFindingsData.muted} Muted` },
|
||||
]}
|
||||
stats={[{ icon: Bell, label: `${passFindingsData.new} New` }]}
|
||||
emptyState={
|
||||
passFindingsData.total === 0
|
||||
? { message: "No passed findings to display" }
|
||||
|
||||
@@ -15,25 +15,31 @@ import {
|
||||
Skeleton,
|
||||
} from "@/components/shadcn";
|
||||
|
||||
// CSS variables are required here as they're passed to RadialChart component
|
||||
// which uses Recharts library that needs actual color values, not Tailwind classes
|
||||
const THREAT_COLORS = {
|
||||
DANGER: "var(--bg-fail-primary)",
|
||||
WARNING: "var(--bg-warning-primary)",
|
||||
SUCCESS: "var(--bg-pass-primary)",
|
||||
NEUTRAL: "var(--bg-neutral-tertiary)",
|
||||
} as const;
|
||||
|
||||
const THREAT_LEVEL_CONFIG = {
|
||||
DANGER: {
|
||||
label: "Critical Risk",
|
||||
color: "var(--bg-fail-primary)",
|
||||
chartColor: "var(--bg-fail-primary)",
|
||||
color: THREAT_COLORS.DANGER,
|
||||
minScore: 0,
|
||||
maxScore: 30,
|
||||
},
|
||||
WARNING: {
|
||||
label: "Moderate Risk",
|
||||
color: "var(--bg-warning-primary)",
|
||||
chartColor: "var(--bg-warning-primary)",
|
||||
color: THREAT_COLORS.WARNING,
|
||||
minScore: 31,
|
||||
maxScore: 60,
|
||||
},
|
||||
SUCCESS: {
|
||||
label: "Secure",
|
||||
color: "var(--bg-pass-primary)",
|
||||
chartColor: "var(--bg-pass-primary)",
|
||||
color: THREAT_COLORS.SUCCESS,
|
||||
minScore: 61,
|
||||
maxScore: 100,
|
||||
},
|
||||
@@ -68,7 +74,7 @@ function convertSectionScoresToTooltipData(
|
||||
return Object.entries(sectionScores).map(([name, value]) => {
|
||||
// Determine color based on the same ranges as THREAT_LEVEL_CONFIG
|
||||
const threatLevel = getThreatLevel(value);
|
||||
const color = THREAT_LEVEL_CONFIG[threatLevel].chartColor;
|
||||
const color = THREAT_LEVEL_CONFIG[threatLevel].color;
|
||||
|
||||
return { name, value, color };
|
||||
});
|
||||
@@ -126,8 +132,8 @@ export function ThreatScore({
|
||||
<RadialChart
|
||||
percentage={displayScore}
|
||||
label="Score"
|
||||
color={config.chartColor}
|
||||
backgroundColor="var(--bg-neutral-tertiary)"
|
||||
color={config.color}
|
||||
backgroundColor={THREAT_COLORS.NEUTRAL}
|
||||
height={206}
|
||||
innerRadius={90}
|
||||
outerRadius={115}
|
||||
@@ -162,8 +168,7 @@ export function ThreatScore({
|
||||
<div className="flex items-start gap-1.5">
|
||||
<ThumbsUp
|
||||
size={16}
|
||||
className="mt-0.5 shrink-0"
|
||||
style={{ minWidth: "16px", minHeight: "16px" }}
|
||||
className="mt-0.5 min-h-4 min-w-4 shrink-0"
|
||||
/>
|
||||
<p>
|
||||
Threat score has{" "}
|
||||
@@ -178,8 +183,7 @@ export function ThreatScore({
|
||||
<div className="flex items-start gap-1.5">
|
||||
<MessageCircleWarning
|
||||
size={16}
|
||||
className="mt-0.5 shrink-0"
|
||||
style={{ minWidth: "16px", minHeight: "16px" }}
|
||||
className="mt-0.5 min-h-4 min-w-4 shrink-0"
|
||||
/>
|
||||
<p>
|
||||
Major gaps include {gaps.slice(0, 2).join(", ")}
|
||||
|
||||
Reference in New Issue
Block a user