From 0b7f02f7e4f39d77bf780f544b56edc266bddf04 Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:38:25 +0200 Subject: [PATCH] feat: Check Findings component (#8976) Co-authored-by: Alan Buscaglia --- .../components/check-findings.tsx | 134 ++++++++++++++++++ ui/app/(prowler)/new-overview/page.tsx | 87 ++++++++++++ ui/components/graphs/donut-chart.tsx | 86 ++++++----- ui/components/graphs/shared/chart-tooltip.tsx | 60 ++------ ui/components/primitives.ts | 53 ------- ui/components/shadcn/README.md | 15 +- .../shadcn/card/base-card/base-card.tsx | 36 +++++ ui/components/shadcn/{ => card}/card.tsx | 9 +- .../resource-stats-card-container.tsx | 0 .../resource-stats-card-content.tsx | 16 +-- .../resource-stats-card-divider.tsx | 0 .../resource-stats-card-header.tsx | 0 .../resource-stats-card.tsx | 4 +- ui/components/shadcn/card/stats-container.tsx | 25 ++++ ui/components/shadcn/index.ts | 29 ++-- .../shadcn/resource-stats-card/index.ts | 13 -- ui/components/ui/chart/Chart.tsx | 3 +- ui/components/ui/custom/custom-button.tsx | 2 +- 18 files changed, 374 insertions(+), 198 deletions(-) create mode 100644 ui/app/(prowler)/new-overview/components/check-findings.tsx create mode 100644 ui/app/(prowler)/new-overview/page.tsx delete mode 100644 ui/components/primitives.ts create mode 100644 ui/components/shadcn/card/base-card/base-card.tsx rename ui/components/shadcn/{ => card}/card.tsx (89%) rename ui/components/shadcn/{ => card}/resource-stats-card/resource-stats-card-container.tsx (100%) rename ui/components/shadcn/{ => card}/resource-stats-card/resource-stats-card-content.tsx (89%) rename ui/components/shadcn/{ => card}/resource-stats-card/resource-stats-card-divider.tsx (100%) rename ui/components/shadcn/{ => card}/resource-stats-card/resource-stats-card-header.tsx (100%) rename ui/components/shadcn/{ => card}/resource-stats-card/resource-stats-card.tsx (98%) create mode 100644 ui/components/shadcn/card/stats-container.tsx delete mode 100644 ui/components/shadcn/resource-stats-card/index.ts diff --git a/ui/app/(prowler)/new-overview/components/check-findings.tsx b/ui/app/(prowler)/new-overview/components/check-findings.tsx new file mode 100644 index 0000000000..e57390afcf --- /dev/null +++ b/ui/app/(prowler)/new-overview/components/check-findings.tsx @@ -0,0 +1,134 @@ +"use client"; + +import { Bell, BellOff, ShieldCheck, TriangleAlert } from "lucide-react"; + +import { DonutChart } from "@/components/graphs/donut-chart"; +import { DonutDataPoint } from "@/components/graphs/types"; +import { + BaseCard, + CardContent, + CardHeader, + CardTitle, + ResourceStatsCard, + StatsContainer, +} from "@/components/shadcn"; +import { CardVariant } from "@/components/shadcn/card/resource-stats-card/resource-stats-card-content"; + +interface CheckFindingsProps { + failFindingsData: { + total: number; + new: number; + muted: number; + }; + passFindingsData: { + total: number; + new: number; + muted: number; + }; +} + +export const CheckFindings = ({ + failFindingsData, + passFindingsData, +}: CheckFindingsProps) => { + // Calculate total findings + const totalFindings = failFindingsData.total + passFindingsData.total; + + // Calculate percentages + const failPercentage = Math.round( + (failFindingsData.total / totalFindings) * 100, + ); + const passPercentage = Math.round( + (passFindingsData.total / totalFindings) * 100, + ); + + // Calculate change percentages (new findings as percentage change) + const failChange = + failFindingsData.total > 0 + ? Math.round((failFindingsData.new / failFindingsData.total) * 100) + : 0; + const passChange = + passFindingsData.total > 0 + ? Math.round((passFindingsData.new / passFindingsData.total) * 100) + : 0; + + // Mock data for DonutChart + const donutData: DonutDataPoint[] = [ + { + name: "Fail Findings", + value: failFindingsData.total, + color: "#f43f5e", // Rose-500 + percentage: Number(failPercentage), + change: Number(failChange), + }, + { + name: "Pass Findings", + value: passFindingsData.total, + color: "#4ade80", // Green-400 + percentage: Number(passPercentage), + change: Number(passChange), + }, + ]; + + return ( + + {/* Header */} + + Check Findings + + + {/* DonutChart Content */} + +
+ +
+ + {/* Footer with ResourceStatsCards */} + + + +
+
+
+ + + + + + ); +}; diff --git a/ui/app/(prowler)/new-overview/page.tsx b/ui/app/(prowler)/new-overview/page.tsx new file mode 100644 index 0000000000..71fc62a024 --- /dev/null +++ b/ui/app/(prowler)/new-overview/page.tsx @@ -0,0 +1,87 @@ +import { Suspense } from "react"; + +import { getFindingsByStatus } from "@/actions/overview/overview"; +import { ContentLayout } from "@/components/ui"; +import { SearchParamsProps } from "@/types"; + +import { CheckFindings } from "./components/check-findings"; + +const FILTER_PREFIX = "filter["; + +// Extract only query params that start with "filter[" for API calls +function pickFilterParams( + params: SearchParamsProps | undefined | null, +): Record { + if (!params) return {}; + return Object.fromEntries( + Object.entries(params).filter(([key]) => key.startsWith(FILTER_PREFIX)), + ); +} + +export default async function NewOverviewPage({ + searchParams, +}: { + searchParams: Promise; +}) { + const resolvedSearchParams = await searchParams; + + return ( + +
+ +

Loading...

+
+ } + > + + +
+ + ); +} + +const SSRCheckFindings = async ({ + searchParams, +}: { + searchParams: SearchParamsProps | undefined | null; +}) => { + const filters = pickFilterParams(searchParams); + + const findingsByStatus = await getFindingsByStatus({ filters }); + + if (!findingsByStatus) { + return ( +
+

Failed to load findings data

+
+ ); + } + + const { + fail = 0, + pass = 0, + muted_new = 0, + muted_changed = 0, + fail_new = 0, + pass_new = 0, + } = findingsByStatus?.data?.attributes || {}; + + const mutedTotal = muted_new + muted_changed; + + return ( + + ); +}; diff --git a/ui/components/graphs/donut-chart.tsx b/ui/components/graphs/donut-chart.tsx index ea5ff49e5f..b82387944f 100644 --- a/ui/components/graphs/donut-chart.tsx +++ b/ui/components/graphs/donut-chart.tsx @@ -21,44 +21,39 @@ interface DonutChartProps { } const CustomTooltip = ({ active, payload }: any) => { - if (active && payload && payload.length) { - const data = payload[0].payload; - return ( -
-
-
- - {data.percentage}% {data.name} - -
- {data.change !== undefined && ( -

- - {data.change > 0 ? "+" : ""} - {data.change}% - {" "} - Since last scan -

- )} + if (!active || !payload || !payload.length) return null; + + const entry = payload[0]; + const name = entry.name; + const percentage = entry.payload?.percentage; + const color = entry.color || entry.payload?.color; + const change = entry.payload?.change; + + return ( +
+
+
+ + {percentage}% + + {name}
- ); - } - return null; +

+ {change !== undefined && ( + <> + + {change > 0 ? "+" : ""} + {change}% + + Since Last Scan + + )} +

+
+ ); }; const CustomLegend = ({ payload }: any) => { @@ -72,8 +67,8 @@ const CustomLegend = ({ payload }: any) => { export function DonutChart({ data, - innerRadius = 80, - outerRadius = 120, + innerRadius = 68, + outerRadius = 86, showLegend = true, centerLabel, }: DonutChartProps) { @@ -108,7 +103,7 @@ export function DonutChart({ })); return ( -
+ <> {chartData.map((entry, index) => { const opacity = @@ -157,9 +152,9 @@ export function DonutChart({ {formattedValue} @@ -167,8 +162,9 @@ export function DonutChart({ {centerLabel.label} @@ -183,6 +179,6 @@ export function DonutChart({ {showLegend && } -
+ ); } diff --git a/ui/components/graphs/shared/chart-tooltip.tsx b/ui/components/graphs/shared/chart-tooltip.tsx index 432a15713d..558da987fe 100644 --- a/ui/components/graphs/shared/chart-tooltip.tsx +++ b/ui/components/graphs/shared/chart-tooltip.tsx @@ -29,7 +29,7 @@ export function ChartTooltip({ return (
)} -

+

{label || data.name}

-

+

{typeof data.value === "number" ? data.value.toLocaleString() : data.value} @@ -62,11 +59,8 @@ export function ChartTooltip({ {data.newFindings !== undefined && data.newFindings > 0 && (

- - + + {data.newFindings} New Findings
@@ -74,11 +68,8 @@ export function ChartTooltip({ {data.new !== undefined && data.new > 0 && (
- - + + {data.new} New
@@ -86,21 +77,15 @@ export function ChartTooltip({ {data.muted !== undefined && data.muted > 0 && (
- - + + {data.muted} Muted
)} {data.change !== undefined && ( -

+

{data.change > 0 ? "+" : ""} {data.change}% @@ -125,17 +110,8 @@ export function MultiSeriesChartTooltip({ } return ( -

-

+

+

{label}

@@ -145,20 +121,14 @@ export function MultiSeriesChartTooltip({ className="h-2 w-2 rounded-full" style={{ backgroundColor: entry.color }} /> - + {entry.name}: - + {entry.value} {entry.payload[`${entry.dataKey}_change`] && ( - + ({entry.payload[`${entry.dataKey}_change`] > 0 ? "+" : ""} {entry.payload[`${entry.dataKey}_change`]}%) diff --git a/ui/components/primitives.ts b/ui/components/primitives.ts deleted file mode 100644 index 04b1f22594..0000000000 --- a/ui/components/primitives.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { tv } from "tailwind-variants"; - -export const title = tv({ - base: "tracking-tight inline font-semibold", - variants: { - color: { - violet: "from-[#FF1CF7] to-[#b249f8]", - yellow: "from-[#FF705B] to-[#FFB457]", - blue: "from-[#5EA2EF] to-[#0072F5]", - cyan: "from-[#00b7fa] to-[#01cfea]", - green: "from-[#6FEE8D] to-[#17c964]", - pink: "from-[#FF72E1] to-[#F54C7A]", - foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", - }, - size: { - sm: "text-3xl lg:text-4xl", - md: "text-[2.3rem] lg:text-5xl leading-9", - lg: "text-4xl lg:text-6xl", - }, - fullWidth: { - true: "w-full block", - }, - }, - defaultVariants: { - size: "md", - }, - compoundVariants: [ - { - color: [ - "violet", - "yellow", - "blue", - "cyan", - "green", - "pink", - "foreground", - ], - class: "bg-clip-text text-transparent bg-linear-to-b", - }, - ], -}); - -export const subtitle = tv({ - base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", - variants: { - fullWidth: { - true: "w-full!", - }, - }, - defaultVariants: { - fullWidth: true, - }, -}); diff --git a/ui/components/shadcn/README.md b/ui/components/shadcn/README.md index 1bd28c8883..af3bc0348d 100644 --- a/ui/components/shadcn/README.md +++ b/ui/components/shadcn/README.md @@ -4,13 +4,18 @@ This directory contains all shadcn/ui based components for the Prowler applicati ## Directory Structure +Example of a custom component: + ``` shadcn/ -├── card.tsx # shadcn Card component -├── resource-stats-card/ # Custom ResourceStatsCard built on shadcn -│ ├── resource-stats-card.tsx -│ ├── resource-stats-card.example.tsx -│ └── index.ts +├── card/ +│ ├── base-card/ +│ │ ├── base-card.tsx +│ ├── card/ +│ │ ├── card.tsx +│ └── resource-stats-card/ +│ ├── resource-stats-card.tsx +│ ├── resource-stats-card.example.tsx ├── index.ts # Barrel exports └── README.md ``` diff --git a/ui/components/shadcn/card/base-card/base-card.tsx b/ui/components/shadcn/card/base-card/base-card.tsx new file mode 100644 index 0000000000..946e0cc184 --- /dev/null +++ b/ui/components/shadcn/card/base-card/base-card.tsx @@ -0,0 +1,36 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +import { Card } from "../card"; + +const baseCardVariants = cva("", { + variants: { + variant: { + default: + "border-slate-200 bg-white dark:border-zinc-900 dark:bg-stone-950", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +interface BaseCardProps + extends React.ComponentProps, + VariantProps {} + +const BaseCard = ({ className, variant, ...props }: BaseCardProps) => { + return ( + + ); +}; + +export { BaseCard }; diff --git a/ui/components/shadcn/card.tsx b/ui/components/shadcn/card/card.tsx similarity index 89% rename from ui/components/shadcn/card.tsx rename to ui/components/shadcn/card/card.tsx index a1b4a7742d..4165221253 100644 --- a/ui/components/shadcn/card.tsx +++ b/ui/components/shadcn/card/card.tsx @@ -1,5 +1,3 @@ -import * as React from "react"; - import { cn } from "@/lib/utils"; function Card({ className, ...props }: React.ComponentProps<"div">) { @@ -20,7 +18,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
) { return (
); diff --git a/ui/components/shadcn/resource-stats-card/resource-stats-card-container.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-container.tsx similarity index 100% rename from ui/components/shadcn/resource-stats-card/resource-stats-card-container.tsx rename to ui/components/shadcn/card/resource-stats-card/resource-stats-card-container.tsx diff --git a/ui/components/shadcn/resource-stats-card/resource-stats-card-content.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx similarity index 89% rename from ui/components/shadcn/resource-stats-card/resource-stats-card-content.tsx rename to ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx index d165eb7944..7f7c0358d9 100644 --- a/ui/components/shadcn/resource-stats-card/resource-stats-card-content.tsx +++ b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-content.tsx @@ -33,11 +33,11 @@ const badgeVariants = cva( { variants: { variant: { - [CardVariant.default]: "bg-[#535359]", - [CardVariant.fail]: "bg-[#432232]", - [CardVariant.pass]: "bg-[#204237]", - [CardVariant.warning]: "bg-[#3d3520]", - [CardVariant.info]: "bg-[#1e3a5f]", + [CardVariant.default]: "bg-slate-100 dark:bg-[#535359]", + [CardVariant.fail]: "bg-red-100 dark:bg-[#432232]", + [CardVariant.pass]: "bg-green-100 dark:bg-[#204237]", + [CardVariant.warning]: "bg-amber-100 dark:bg-[#3d3520]", + [CardVariant.info]: "bg-blue-100 dark:bg-[#1e3a5f]", }, size: { sm: "px-1 text-xs", @@ -66,7 +66,7 @@ const badgeIconVariants = cva("", { }); const labelTextVariants = cva( - "leading-6 font-semibold text-zinc-300 dark:text-zinc-300", + "leading-6 font-semibold text-slate-900 dark:text-zinc-300 whitespace-nowrap", { variants: { size: { @@ -81,7 +81,7 @@ const labelTextVariants = cva( }, ); -const statIconVariants = cva("text-zinc-300 dark:text-zinc-300", { +const statIconVariants = cva("text-slate-600 dark:text-zinc-300", { variants: { size: { sm: "h-2.5 w-2.5", @@ -95,7 +95,7 @@ const statIconVariants = cva("text-zinc-300 dark:text-zinc-300", { }); const statLabelVariants = cva( - "leading-5 font-medium text-zinc-300 dark:text-zinc-300", + "leading-5 font-medium text-slate-700 dark:text-zinc-300", { variants: { size: { diff --git a/ui/components/shadcn/resource-stats-card/resource-stats-card-divider.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-divider.tsx similarity index 100% rename from ui/components/shadcn/resource-stats-card/resource-stats-card-divider.tsx rename to ui/components/shadcn/card/resource-stats-card/resource-stats-card-divider.tsx diff --git a/ui/components/shadcn/resource-stats-card/resource-stats-card-header.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card-header.tsx similarity index 100% rename from ui/components/shadcn/resource-stats-card/resource-stats-card-header.tsx rename to ui/components/shadcn/card/resource-stats-card/resource-stats-card-header.tsx diff --git a/ui/components/shadcn/resource-stats-card/resource-stats-card.tsx b/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx similarity index 98% rename from ui/components/shadcn/resource-stats-card/resource-stats-card.tsx rename to ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx index 7acbd103e3..4a1520d44d 100644 --- a/ui/components/shadcn/resource-stats-card/resource-stats-card.tsx +++ b/ui/components/shadcn/card/resource-stats-card/resource-stats-card.tsx @@ -111,7 +111,7 @@ export const ResourceStatsCard = ({ {header && } {emptyState ? (
-

+

{emptyState.message}

@@ -141,7 +141,7 @@ export const ResourceStatsCard = ({ {header && } {emptyState ? (
-

+

{emptyState.message}

diff --git a/ui/components/shadcn/card/stats-container.tsx b/ui/components/shadcn/card/stats-container.tsx new file mode 100644 index 0000000000..9d37a60897 --- /dev/null +++ b/ui/components/shadcn/card/stats-container.tsx @@ -0,0 +1,25 @@ +import { cn } from "@/lib/utils"; + +interface StatsContainerProps extends React.HTMLAttributes { + children: React.ReactNode; +} + +const StatsContainer = ({ + className, + children, + ...props +}: StatsContainerProps) => { + return ( +
+ {children} +
+ ); +}; + +export { StatsContainer }; diff --git a/ui/components/shadcn/index.ts b/ui/components/shadcn/index.ts index 4291e07d5a..d29dc6f1dc 100644 --- a/ui/components/shadcn/index.ts +++ b/ui/components/shadcn/index.ts @@ -1,21 +1,8 @@ -export { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "./card"; -export { - ResourceStatsCard, - ResourceStatsCardContainer, - type ResourceStatsCardContainerProps, - ResourceStatsCardContent, - type ResourceStatsCardContentProps, - ResourceStatsCardDivider, - type ResourceStatsCardDividerProps, - ResourceStatsCardHeader, - type ResourceStatsCardHeaderProps, - type ResourceStatsCardProps, - type StatItem, -} from "./resource-stats-card"; +export * from "./card/base-card/base-card"; +export * from "./card/card"; +export * from "./card/resource-stats-card/resource-stats-card"; +export * from "./card/resource-stats-card/resource-stats-card-container"; +export * from "./card/resource-stats-card/resource-stats-card-content"; +export * from "./card/resource-stats-card/resource-stats-card-divider"; +export * from "./card/resource-stats-card/resource-stats-card-header"; +export * from "./card/stats-container"; diff --git a/ui/components/shadcn/resource-stats-card/index.ts b/ui/components/shadcn/resource-stats-card/index.ts deleted file mode 100644 index c049987ae0..0000000000 --- a/ui/components/shadcn/resource-stats-card/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type { ResourceStatsCardProps } from "./resource-stats-card"; -export { ResourceStatsCard } from "./resource-stats-card"; -export type { ResourceStatsCardContainerProps } from "./resource-stats-card-container"; -export { ResourceStatsCardContainer } from "./resource-stats-card-container"; -export type { - ResourceStatsCardContentProps, - StatItem, -} from "./resource-stats-card-content"; -export { ResourceStatsCardContent } from "./resource-stats-card-content"; -export type { ResourceStatsCardDividerProps } from "./resource-stats-card-divider"; -export { ResourceStatsCardDivider } from "./resource-stats-card-divider"; -export type { ResourceStatsCardHeaderProps } from "./resource-stats-card-header"; -export { ResourceStatsCardHeader } from "./resource-stats-card-header"; diff --git a/ui/components/ui/chart/Chart.tsx b/ui/components/ui/chart/Chart.tsx index fec25668b6..014fbc009d 100644 --- a/ui/components/ui/chart/Chart.tsx +++ b/ui/components/ui/chart/Chart.tsx @@ -70,6 +70,7 @@ const ChartContainer = React.forwardRef< ); }); + ChartContainer.displayName = "Chart"; const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { @@ -184,7 +185,7 @@ const ChartTooltipContent = React.forwardRef<
diff --git a/ui/components/ui/custom/custom-button.tsx b/ui/components/ui/custom/custom-button.tsx index 19d50ba686..8f71aabf24 100644 --- a/ui/components/ui/custom/custom-button.tsx +++ b/ui/components/ui/custom/custom-button.tsx @@ -86,7 +86,7 @@ export const CustomButton = React.forwardRef< ) => (