mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(ui): add provider filtering to the compliance overview
- Mount provider type/account and provider group selectors in the compliance filter bar; selecting a scan or provider filter clears the other (XOR) - Aggregate frameworks across the latest scan per matching provider; hide the ThreatScore badge while provider filters are active - Hide the per-scan CIS PDF and carry provider filters into the card drill-down in aggregated mode Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,4 +13,15 @@ describe("Compliance overview page", () => {
|
||||
expect(source).toContain("ComplianceOverviewGrid");
|
||||
expect(source).not.toContain("filter[search]");
|
||||
});
|
||||
|
||||
it("switches to aggregated mode when provider filters are present", () => {
|
||||
expect(source).toContain("hasComplianceProviderFilters");
|
||||
expect(source).toContain("extractComplianceProviderFilters");
|
||||
});
|
||||
|
||||
it("feeds the provider and provider-group selectors", () => {
|
||||
expect(source).toContain("getAllProviders");
|
||||
expect(source).toContain("getAllProviderGroups");
|
||||
expect(source).toContain("providerGroups={");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
getComplianceOverviewMetadataInfo,
|
||||
getCompliancesOverview,
|
||||
} from "@/actions/compliances";
|
||||
import { getAllProviderGroups } from "@/actions/manage-groups/manage-groups";
|
||||
import { getThreatScore } from "@/actions/overview";
|
||||
import { getAllProviders } from "@/actions/providers/providers";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import {
|
||||
ComplianceSkeletonGrid,
|
||||
@@ -17,6 +19,10 @@ import { ComplianceOverviewGrid } from "@/components/compliance/compliance-overv
|
||||
import { Alert, AlertDescription } from "@/components/shadcn/alert";
|
||||
import { Card, CardContent } from "@/components/shadcn/card/card";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import {
|
||||
extractComplianceProviderFilters,
|
||||
hasComplianceProviderFilters,
|
||||
} from "@/lib/compliance/compliance-provider-filters";
|
||||
import { pickLatestCisPerProvider } from "@/lib/compliance/compliance-report-types";
|
||||
import {
|
||||
ExpandedScanData,
|
||||
@@ -34,16 +40,24 @@ export default async function Compliance({
|
||||
const resolvedSearchParams = await searchParams;
|
||||
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
|
||||
|
||||
const scansData = await getScans({
|
||||
filters: {
|
||||
"filter[state]": "completed",
|
||||
},
|
||||
pageSize: 50,
|
||||
fields: {
|
||||
scans: "name,completed_at,provider",
|
||||
},
|
||||
include: "provider",
|
||||
});
|
||||
const hasProviderFilters = hasComplianceProviderFilters(resolvedSearchParams);
|
||||
const providerFilters =
|
||||
extractComplianceProviderFilters(resolvedSearchParams);
|
||||
|
||||
const [scansData, providersData, providerGroupsData] = await Promise.all([
|
||||
getScans({
|
||||
filters: {
|
||||
"filter[state]": "completed",
|
||||
},
|
||||
pageSize: 50,
|
||||
fields: {
|
||||
scans: "name,completed_at,provider",
|
||||
},
|
||||
include: "provider",
|
||||
}),
|
||||
getAllProviders(),
|
||||
getAllProviderGroups(),
|
||||
]);
|
||||
|
||||
if (!scansData?.data) {
|
||||
return (
|
||||
@@ -90,9 +104,12 @@ export default async function Compliance({
|
||||
const scanIdFromUrl = Array.isArray(scanIdParam)
|
||||
? scanIdParam[0]
|
||||
: scanIdParam;
|
||||
const selectedScanId: string | null =
|
||||
scanIdFromUrl || expandedScansData[0]?.id || null;
|
||||
const onboardingAction = selectedScanId
|
||||
// Aggregated mode ignores scanId entirely (backend XOR); provider filters drive scope.
|
||||
const selectedScanId: string | null = hasProviderFilters
|
||||
? null
|
||||
: scanIdFromUrl || expandedScansData[0]?.id || null;
|
||||
const hasScope = hasProviderFilters || Boolean(selectedScanId);
|
||||
const onboardingAction = hasScope
|
||||
? { flowId: "view-compliance" }
|
||||
: {
|
||||
flowId: "view-compliance",
|
||||
@@ -115,13 +132,15 @@ export default async function Compliance({
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const metadataInfoData = selectedScanId
|
||||
? await getComplianceOverviewMetadataInfo({
|
||||
filters: {
|
||||
"filter[scan_id]": selectedScanId,
|
||||
},
|
||||
})
|
||||
: { data: { attributes: { regions: [] } } };
|
||||
const metadataInfoData = hasProviderFilters
|
||||
? await getComplianceOverviewMetadataInfo({ filters: providerFilters })
|
||||
: selectedScanId
|
||||
? await getComplianceOverviewMetadataInfo({
|
||||
filters: {
|
||||
"filter[scan_id]": selectedScanId,
|
||||
},
|
||||
})
|
||||
: { data: { attributes: { regions: [] } } };
|
||||
|
||||
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
|
||||
|
||||
@@ -146,13 +165,15 @@ export default async function Compliance({
|
||||
icon="lucide:shield-check"
|
||||
onboardingAction={onboardingAction}
|
||||
>
|
||||
{selectedScanId ? (
|
||||
{hasScope ? (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<ComplianceFilters
|
||||
scans={expandedScansData}
|
||||
uniqueRegions={uniqueRegions}
|
||||
selectedScanId={selectedScanId}
|
||||
providers={providersData?.data ?? []}
|
||||
providerGroups={providerGroupsData?.data ?? []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -182,6 +203,8 @@ export default async function Compliance({
|
||||
searchParams={resolvedSearchParams}
|
||||
scanId={selectedScanId}
|
||||
selectedScan={selectedScanData}
|
||||
hasProviderFilters={hasProviderFilters}
|
||||
providerFilters={providerFilters}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
@@ -196,15 +219,23 @@ const SSRComplianceGrid = async ({
|
||||
searchParams,
|
||||
scanId,
|
||||
selectedScan,
|
||||
hasProviderFilters,
|
||||
providerFilters,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
scanId: string | null;
|
||||
selectedScan?: ScanEntity;
|
||||
hasProviderFilters: boolean;
|
||||
providerFilters: Record<string, string>;
|
||||
}) => {
|
||||
const regionFilter = searchParams["filter[region__in]"]?.toString() || "";
|
||||
|
||||
const compliancesData =
|
||||
scanId && scanId.trim() !== ""
|
||||
const compliancesData = hasProviderFilters
|
||||
? await getCompliancesOverview({
|
||||
region: regionFilter,
|
||||
filters: providerFilters,
|
||||
})
|
||||
: scanId && scanId.trim() !== ""
|
||||
? await getCompliancesOverview({
|
||||
scanId,
|
||||
region: regionFilter,
|
||||
@@ -230,8 +261,9 @@ const SSRComplianceGrid = async ({
|
||||
<Alert variant="info">
|
||||
<Info className="size-4" />
|
||||
<AlertDescription>
|
||||
This scan has no compliance data available yet, please select a
|
||||
different one.
|
||||
{hasProviderFilters
|
||||
? "No completed scans match the selected providers."
|
||||
: "This scan has no compliance data available yet, please select a different one."}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -27,4 +27,21 @@ describe("ComplianceCard", () => {
|
||||
expect(source).toContain('orientation="column"');
|
||||
expect(source).toContain('buttonWidth="icon"');
|
||||
});
|
||||
|
||||
it("derives aggregated mode from the provider filters in the URL", () => {
|
||||
expect(source).toContain("extractComplianceProviderFilters");
|
||||
expect(source).toContain("const isAggregated =");
|
||||
});
|
||||
|
||||
it("hides the per-scan PDF download in aggregated mode", () => {
|
||||
expect(source).toContain("{!isAggregated && (");
|
||||
expect(source).toContain("<ComplianceDownloadContainer");
|
||||
});
|
||||
|
||||
it("carries provider filters (not scanId) into the drill-down when aggregated", () => {
|
||||
expect(source).toContain(
|
||||
"providerFilters: isAggregated ? providerFilters : undefined",
|
||||
);
|
||||
expect(source).toContain("scanId: isAggregated ? null : scanId");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { buildComplianceDetailPath } from "@/lib/compliance/compliance-detail-url";
|
||||
import { extractComplianceProviderFilters } from "@/lib/compliance/compliance-provider-filters";
|
||||
import { getReportTypeForCompliance } from "@/lib/compliance/compliance-report-types";
|
||||
import {
|
||||
getScoreIndicatorClass,
|
||||
@@ -55,6 +56,13 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
const router = useRouter();
|
||||
const hasRegionFilter = searchParams.has("filter[region__in]");
|
||||
|
||||
// Aggregated mode: provider filters replace the single-scan scope, so per-scan
|
||||
// affordances (CIS PDF) are hidden and the drill-down carries provider filters.
|
||||
const providerFilters = extractComplianceProviderFilters(
|
||||
new URLSearchParams(searchParams.toString()),
|
||||
);
|
||||
const isAggregated = Object.keys(providerFilters).length > 0;
|
||||
|
||||
const formatTitle = (title: string) => {
|
||||
return title.split("-").join(" ");
|
||||
};
|
||||
@@ -75,8 +83,9 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
title,
|
||||
complianceId: id,
|
||||
version,
|
||||
scanId,
|
||||
scanId: isAggregated ? null : scanId,
|
||||
regionFilter: searchParams.get("filter[region__in]"),
|
||||
providerFilters: isAggregated ? providerFilters : undefined,
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -88,32 +97,34 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
className="relative cursor-pointer transition-shadow hover:shadow-md"
|
||||
onClick={navigateToDetail}
|
||||
>
|
||||
<div
|
||||
className="absolute top-2 right-2 z-10"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
role="group"
|
||||
tabIndex={0}
|
||||
>
|
||||
<ComplianceDownloadContainer
|
||||
compact
|
||||
orientation="column"
|
||||
buttonWidth="icon"
|
||||
presentation="dropdown"
|
||||
scanId={scanId}
|
||||
complianceId={complianceId}
|
||||
reportType={getReportTypeForCompliance(
|
||||
title,
|
||||
complianceId,
|
||||
isLatestCisForProvider,
|
||||
)}
|
||||
disabled={hasRegionFilter}
|
||||
/>
|
||||
</div>
|
||||
{!isAggregated && (
|
||||
<div
|
||||
className="absolute top-2 right-2 z-10"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
role="group"
|
||||
tabIndex={0}
|
||||
>
|
||||
<ComplianceDownloadContainer
|
||||
compact
|
||||
orientation="column"
|
||||
buttonWidth="icon"
|
||||
presentation="dropdown"
|
||||
scanId={scanId}
|
||||
complianceId={complianceId}
|
||||
reportType={getReportTypeForCompliance(
|
||||
title,
|
||||
complianceId,
|
||||
isLatestCisForProvider,
|
||||
)}
|
||||
disabled={hasRegionFilter}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<CardContent className="p-0">
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<div className="flex items-center gap-3 pr-9">
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ComplianceFilters } from "./compliance-filters";
|
||||
|
||||
const { pushMock, updateFilterMock } = vi.hoisted(() => ({
|
||||
pushMock: vi.fn(),
|
||||
updateFilterMock: vi.fn(),
|
||||
}));
|
||||
|
||||
let currentSearchParams = new URLSearchParams();
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: pushMock }),
|
||||
useSearchParams: () => currentSearchParams,
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-url-filters", () => ({
|
||||
useUrlFilters: () => ({ updateFilter: updateFilterMock }),
|
||||
}));
|
||||
|
||||
vi.mock("./scan-selector", () => ({
|
||||
ScanSelector: ({
|
||||
onSelectionChange,
|
||||
}: {
|
||||
onSelectionChange: (key: string) => void;
|
||||
}) => (
|
||||
<button
|
||||
data-testid="scan-selector"
|
||||
onClick={() => onSelectionChange("scan-2")}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/filters/provider-account-selectors", () => ({
|
||||
ProviderAccountSelectors: ({
|
||||
paramsToDeleteOnChange,
|
||||
}: {
|
||||
paramsToDeleteOnChange?: string[];
|
||||
}) => (
|
||||
<div
|
||||
data-testid="provider-account-selectors"
|
||||
data-params={(paramsToDeleteOnChange ?? []).join(",")}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/app/(prowler)/_overview/_components/provider-group-selector",
|
||||
() => ({
|
||||
ProviderGroupSelector: ({
|
||||
paramsToDeleteOnChange,
|
||||
}: {
|
||||
paramsToDeleteOnChange?: string[];
|
||||
}) => (
|
||||
<div
|
||||
data-testid="provider-group-selector"
|
||||
data-params={(paramsToDeleteOnChange ?? []).join(",")}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("@/components/filters/clear-filters-button", () => ({
|
||||
ClearFiltersButton: () => <div data-testid="clear-filters" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn/select/multiselect", () => ({
|
||||
MultiSelect: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="region-multiselect">{children}</div>
|
||||
),
|
||||
MultiSelectTrigger: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
MultiSelectValue: ({ placeholder }: { placeholder: string }) => (
|
||||
<span>{placeholder}</span>
|
||||
),
|
||||
MultiSelectContent: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
MultiSelectItem: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
MultiSelectSelectAll: ({ children }: { children: React.ReactNode }) => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
MultiSelectSeparator: () => <hr />,
|
||||
}));
|
||||
|
||||
const defaultProps = {
|
||||
scans: [],
|
||||
uniqueRegions: ["eu-west-1"],
|
||||
selectedScanId: "scan-1",
|
||||
providers: [],
|
||||
providerGroups: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
currentSearchParams = new URLSearchParams();
|
||||
});
|
||||
|
||||
describe("ComplianceFilters", () => {
|
||||
it("renders the scan, provider type/account, provider group and region selectors", () => {
|
||||
render(<ComplianceFilters {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("scan-selector")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("provider-account-selectors"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId("provider-group-selector")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("region-multiselect")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("wires the provider selectors to clear scanId + page on change (reverse XOR)", () => {
|
||||
render(<ComplianceFilters {...defaultProps} />);
|
||||
|
||||
for (const testId of [
|
||||
"provider-account-selectors",
|
||||
"provider-group-selector",
|
||||
]) {
|
||||
const params = screen.getByTestId(testId).getAttribute("data-params");
|
||||
expect(params).toContain("scanId");
|
||||
expect(params).toContain("page");
|
||||
}
|
||||
});
|
||||
|
||||
it("clears provider-scope filters and page when a scan is selected", () => {
|
||||
currentSearchParams = new URLSearchParams(
|
||||
"scanId=scan-1&filter[provider_type__in]=aws&filter[provider_id__in]=p1&filter[provider_groups__in]=g1&filter[region__in]=eu-west-1&page=2",
|
||||
);
|
||||
|
||||
render(<ComplianceFilters {...defaultProps} />);
|
||||
fireEvent.click(screen.getByTestId("scan-selector"));
|
||||
|
||||
expect(pushMock).toHaveBeenCalledTimes(1);
|
||||
const pushedUrl = new URL(
|
||||
pushMock.mock.calls[0][0] as string,
|
||||
"https://example.com",
|
||||
);
|
||||
const params = pushedUrl.searchParams;
|
||||
|
||||
expect(params.get("scanId")).toBe("scan-2");
|
||||
expect(params.get("filter[provider_type__in]")).toBeNull();
|
||||
expect(params.get("filter[provider_id__in]")).toBeNull();
|
||||
expect(params.get("filter[provider_groups__in]")).toBeNull();
|
||||
expect(params.get("page")).toBeNull();
|
||||
// region is independent of the scan/provider XOR and must survive
|
||||
expect(params.get("filter[region__in]")).toBe("eu-west-1");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { ProviderGroupSelector } from "@/app/(prowler)/_overview/_components/provider-group-selector";
|
||||
import { ClearFiltersButton } from "@/components/filters/clear-filters-button";
|
||||
import { ProviderAccountSelectors } from "@/components/filters/provider-account-selectors";
|
||||
import {
|
||||
MultiSelect,
|
||||
MultiSelectContent,
|
||||
@@ -13,27 +15,43 @@ import {
|
||||
MultiSelectValue,
|
||||
} from "@/components/shadcn/select/multiselect";
|
||||
import { useUrlFilters } from "@/hooks/use-url-filters";
|
||||
import { COMPLIANCE_PROVIDER_FILTER_KEYS } from "@/lib/compliance/compliance-provider-filters";
|
||||
import type { ProviderGroup } from "@/types/components";
|
||||
import type { ProviderProps } from "@/types/providers";
|
||||
|
||||
import { ScanSelector, SelectScanComplianceDataProps } from "./scan-selector";
|
||||
|
||||
// Clearing scanId/page is the inverse of the provider selectors'
|
||||
// paramsToDeleteOnChange — together they enforce the backend scan_id ⊕ provider XOR.
|
||||
const PROVIDER_PARAMS_TO_DELETE = ["scanId", "page"];
|
||||
const SELECTOR_WIDTH = "w-full sm:max-w-[280px] sm:min-w-[200px] sm:flex-1";
|
||||
|
||||
interface ComplianceFiltersProps {
|
||||
scans: SelectScanComplianceDataProps["scans"];
|
||||
uniqueRegions: string[];
|
||||
selectedScanId: string;
|
||||
/** Null in aggregated mode (provider filters drive the scope, no single scan). */
|
||||
selectedScanId: string | null;
|
||||
providers: ProviderProps[];
|
||||
providerGroups: ProviderGroup[];
|
||||
}
|
||||
|
||||
export const ComplianceFilters = ({
|
||||
scans,
|
||||
uniqueRegions,
|
||||
selectedScanId,
|
||||
providers,
|
||||
providerGroups,
|
||||
}: ComplianceFiltersProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { updateFilter } = useUrlFilters();
|
||||
|
||||
// XOR: choosing a single scan clears any active provider-scope filters.
|
||||
const handleScanChange = (selectedKey: string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("scanId", selectedKey);
|
||||
COMPLIANCE_PROVIDER_FILTER_KEYS.forEach((key) => params.delete(key));
|
||||
params.delete("page");
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
@@ -41,16 +59,31 @@ export const ComplianceFilters = ({
|
||||
searchParams.get("filter[region__in]")?.split(",").filter(Boolean) ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex max-w-4xl flex-wrap items-center gap-4">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="w-full sm:max-w-[380px] sm:min-w-[200px] sm:flex-1">
|
||||
<ScanSelector
|
||||
scans={scans}
|
||||
selectedScanId={selectedScanId}
|
||||
selectedScanId={selectedScanId ?? ""}
|
||||
onSelectionChange={handleScanChange}
|
||||
/>
|
||||
</div>
|
||||
{/* Provider-scope filters: selecting any switches to aggregated mode and clears scanId. */}
|
||||
<ProviderAccountSelectors
|
||||
providers={providers}
|
||||
accountFilterKey="provider_id__in"
|
||||
accountValue="id"
|
||||
paramsToDeleteOnChange={PROVIDER_PARAMS_TO_DELETE}
|
||||
providerSelectorClassName={SELECTOR_WIDTH}
|
||||
accountSelectorClassName={SELECTOR_WIDTH}
|
||||
/>
|
||||
<div className={SELECTOR_WIDTH}>
|
||||
<ProviderGroupSelector
|
||||
groups={providerGroups}
|
||||
paramsToDeleteOnChange={PROVIDER_PARAMS_TO_DELETE}
|
||||
/>
|
||||
</div>
|
||||
{uniqueRegions.length > 0 && (
|
||||
<div className="w-full sm:max-w-[280px] sm:min-w-[200px] sm:flex-1">
|
||||
<div className={SELECTOR_WIDTH}>
|
||||
<MultiSelect
|
||||
values={regionValues}
|
||||
onValuesChange={(values) => updateFilter("region__in", values)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ComplianceCard } from "@/components/compliance/compliance-card";
|
||||
import { OnboardingTrigger, PageReady } from "@/components/onboarding";
|
||||
import { DataTableSearch } from "@/components/ui/table/data-table-search";
|
||||
import { buildComplianceDetailPath } from "@/lib/compliance/compliance-detail-url";
|
||||
import { extractComplianceProviderFilters } from "@/lib/compliance/compliance-provider-filters";
|
||||
import { getFlowById } from "@/lib/onboarding";
|
||||
import { createViewComplianceTourStepHandlers } from "@/lib/tours/view-compliance.tour";
|
||||
import type { ComplianceOverviewData } from "@/types/compliance";
|
||||
@@ -43,6 +44,12 @@ export const ComplianceOverviewGrid = ({
|
||||
const searchParams = useSearchParams();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// Aggregated mode: provider filters in the URL replace the single-scan scope.
|
||||
const providerFilters = extractComplianceProviderFilters(
|
||||
new URLSearchParams(searchParams.toString()),
|
||||
);
|
||||
const isAggregated = Object.keys(providerFilters).length > 0;
|
||||
|
||||
const filteredFrameworks = frameworks.filter((compliance) =>
|
||||
compliance.attributes.framework
|
||||
.toLowerCase()
|
||||
@@ -62,8 +69,9 @@ export const ComplianceOverviewGrid = ({
|
||||
title: first.attributes.framework,
|
||||
complianceId: first.id,
|
||||
version: first.attributes.version,
|
||||
scanId,
|
||||
scanId: isAggregated ? null : scanId,
|
||||
regionFilter: searchParams.get("filter[region__in]"),
|
||||
providerFilters: isAggregated ? providerFilters : undefined,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user