diff --git a/ui/app/(prowler)/new-overview/components/accounts-selector.tsx b/ui/app/(prowler)/new-overview/components/accounts-selector.tsx new file mode 100644 index 0000000000..0f571ae3e0 --- /dev/null +++ b/ui/app/(prowler)/new-overview/components/accounts-selector.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { ReactNode } from "react"; + +import { + AWSProviderBadge, + AzureProviderBadge, + GCPProviderBadge, + GitHubProviderBadge, + KS8ProviderBadge, + M365ProviderBadge, +} from "@/components/icons/providers-badge"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn"; +import type { ProviderProps, ProviderType } from "@/types/providers"; + +const PROVIDER_ICON: Record = { + aws: , + azure: , + gcp: , + kubernetes: , + m365: , + github: , +}; + +interface AccountsSelectorProps { + providers: ProviderProps[]; +} + +export function AccountsSelector({ providers }: AccountsSelectorProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const current = searchParams.get("filter[provider_id__in]") || ""; + const selectedTypes = searchParams.get("filter[provider_type__in]") || ""; + const selectedTypesList = selectedTypes + ? selectedTypes.split(",").filter(Boolean) + : []; + const selectedIds = current ? current.split(",").filter(Boolean) : []; + const visibleProviders = providers + .filter((p) => p.attributes.connection?.connected) + .filter((p) => + selectedTypesList.length > 0 + ? selectedTypesList.includes(p.attributes.provider) + : true, + ); + + const handleMultiValueChange = (ids: string[]) => { + const params = new URLSearchParams(searchParams.toString()); + if (ids.length > 0) { + params.set("filter[provider_id__in]", ids.join(",")); + } else { + params.delete("filter[provider_id__in]"); + } + + // Auto-deselect provider types that no longer have any selected accounts + if (selectedTypesList.length > 0) { + // Get provider types of currently selected accounts + const selectedProviders = providers.filter((p) => ids.includes(p.id)); + const selectedProviderTypes = new Set( + selectedProviders.map((p) => p.attributes.provider), + ); + + // Keep only provider types that still have selected accounts + const remainingProviderTypes = selectedTypesList.filter((type) => + selectedProviderTypes.has(type as ProviderType), + ); + + // Update provider_type__in filter + if (remainingProviderTypes.length > 0) { + params.set( + "filter[provider_type__in]", + remainingProviderTypes.join(","), + ); + } else { + params.delete("filter[provider_type__in]"); + } + } + + router.push(`?${params.toString()}`, { scroll: false }); + }; + + const selectedLabel = () => { + if (selectedIds.length === 0) return null; // placeholder visible + if (selectedIds.length === 1) { + const p = providers.find((pr) => pr.id === selectedIds[0]); + const name = p ? p.attributes.alias || p.attributes.uid : selectedIds[0]; + return {name}; + } + return ( + {selectedIds.length} accounts selected + ); + }; + + const filterDescription = + selectedTypesList.length > 0 + ? `Showing accounts for ${selectedTypesList.join(", ")} providers` + : "All connected cloud provider accounts"; + + return ( +
+ + +
+ ); +} diff --git a/ui/app/(prowler)/new-overview/components/check-findings.tsx b/ui/app/(prowler)/new-overview/components/check-findings.tsx index e57390afcf..4f431b6229 100644 --- a/ui/app/(prowler)/new-overview/components/check-findings.tsx +++ b/ui/app/(prowler)/new-overview/components/check-findings.tsx @@ -9,10 +9,10 @@ import { CardContent, CardHeader, CardTitle, + CardVariant, ResourceStatsCard, - StatsContainer, + ResourceStatsCardContainer, } from "@/components/shadcn"; -import { CardVariant } from "@/components/shadcn/card/resource-stats-card/resource-stats-card-content"; interface CheckFindingsProps { failFindingsData: { @@ -93,7 +93,7 @@ export const CheckFindings = ({ {/* Footer with ResourceStatsCards */} - + -
-
+
+
- + ); diff --git a/ui/app/(prowler)/new-overview/components/provider-type-selector.tsx b/ui/app/(prowler)/new-overview/components/provider-type-selector.tsx new file mode 100644 index 0000000000..25eb6dbbc7 --- /dev/null +++ b/ui/app/(prowler)/new-overview/components/provider-type-selector.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { lazy, Suspense } from "react"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/shadcn"; +import { type ProviderProps, ProviderType } from "@/types/providers"; + +const AWSProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.AWSProviderBadge, + })), +); +const AzureProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.AzureProviderBadge, + })), +); +const GCPProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.GCPProviderBadge, + })), +); +const KS8ProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.KS8ProviderBadge, + })), +); +const M365ProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.M365ProviderBadge, + })), +); +const GitHubProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.GitHubProviderBadge, + })), +); + +type IconProps = { width: number; height: number }; + +const IconPlaceholder = ({ width, height }: IconProps) => ( +
+); + +const PROVIDER_DATA: Record< + ProviderType, + { label: string; icon: React.ComponentType } +> = { + aws: { + label: "Amazon Web Services", + icon: AWSProviderBadge, + }, + azure: { + label: "Microsoft Azure", + icon: AzureProviderBadge, + }, + gcp: { + label: "Google Cloud Platform", + icon: GCPProviderBadge, + }, + kubernetes: { + label: "Kubernetes", + icon: KS8ProviderBadge, + }, + m365: { + label: "Microsoft 365", + icon: M365ProviderBadge, + }, + github: { + label: "GitHub", + icon: GitHubProviderBadge, + }, +}; + +type ProviderTypeSelectorProps = { + providers: ProviderProps[]; +}; + +export const ProviderTypeSelector = ({ + providers, +}: ProviderTypeSelectorProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const currentProviders = searchParams.get("filter[provider_type__in]") || ""; + const selectedTypes = currentProviders + ? currentProviders.split(",").filter(Boolean) + : []; + + const handleMultiValueChange = (values: string[]) => { + const params = new URLSearchParams(searchParams.toString()); + + // Update provider_type__in + if (values.length > 0) { + params.set("filter[provider_type__in]", values.join(",")); + } else { + params.delete("filter[provider_type__in]"); + } + + // Clear account selection when changing provider types + // User should manually select accounts if they want to filter by specific accounts + params.delete("filter[provider_id__in]"); + + router.push(`?${params.toString()}`, { scroll: false }); + }; + + const availableTypes = Array.from( + new Set( + providers + .filter((p) => p.attributes.connection?.connected) + .map((p) => p.attributes.provider), + ), + ) as ProviderType[]; + + const renderIcon = (providerType: ProviderType) => { + const IconComponent = PROVIDER_DATA[providerType].icon; + return ( + }> + + + ); + }; + + const selectedLabel = () => { + if (selectedTypes.length === 0) return null; // placeholder visible + if (selectedTypes.length === 1) { + const providerType = selectedTypes[0] as ProviderType; + return ( + + {renderIcon(providerType)} + {PROVIDER_DATA[providerType].label} + + ); + } + return ( + + {selectedTypes.length} providers selected + + ); + }; + + return ( +
+ + +
+ ); +}; diff --git a/ui/app/(prowler)/new-overview/page.tsx b/ui/app/(prowler)/new-overview/page.tsx index 71fc62a024..ba0cb8362c 100644 --- a/ui/app/(prowler)/new-overview/page.tsx +++ b/ui/app/(prowler)/new-overview/page.tsx @@ -1,10 +1,13 @@ import { Suspense } from "react"; import { getFindingsByStatus } from "@/actions/overview/overview"; +import { getProviders } from "@/actions/providers"; import { ContentLayout } from "@/components/ui"; import { SearchParamsProps } from "@/types"; +import { AccountsSelector } from "./components/accounts-selector"; import { CheckFindings } from "./components/check-findings"; +import { ProviderTypeSelector } from "./components/provider-type-selector"; const FILTER_PREFIX = "filter["; @@ -24,10 +27,15 @@ export default async function NewOverviewPage({ searchParams: Promise; }) { const resolvedSearchParams = await searchParams; + const providersData = await getProviders({ page: 1, pageSize: 200 }); return ( -
+
+ + +
+
diff --git a/ui/components/graphs/donut-chart.tsx b/ui/components/graphs/donut-chart.tsx index b82387944f..5643b9e2f6 100644 --- a/ui/components/graphs/donut-chart.tsx +++ b/ui/components/graphs/donut-chart.tsx @@ -94,11 +94,25 @@ export function DonutChart({ change: item.change, })); - const legendPayload = chartData.map((entry) => ({ - value: entry.name, + const total = chartData.reduce((sum, d) => sum + (Number(d.value) || 0), 0); + const isEmpty = total <= 0; + + const emptyData = [ + { + name: "No data", + value: 1, + fill: "var(--chart-border-emphasis)", + color: "var(--chart-border-emphasis)", + percentage: 0, + change: undefined, + }, + ]; + + const legendPayload = (isEmpty ? emptyData : chartData).map((entry) => ({ + value: isEmpty ? "No data" : entry.name, color: entry.color, payload: { - percentage: entry.percentage, + percentage: isEmpty ? 0 : entry.percentage, }, })); @@ -109,9 +123,9 @@ export function DonutChart({ className="mx-auto aspect-square max-h-[350px]" > - } /> + {!isEmpty && } />} - {chartData.map((entry, index) => { + {(isEmpty ? emptyData : chartData).map((entry, index) => { const opacity = hoveredIndex === null ? 1 : hoveredIndex === index ? 1 : 0.5; return ( @@ -133,14 +147,18 @@ export function DonutChart({ /> ); })} - {centerLabel && ( + {(centerLabel || isEmpty) && (