diff --git a/ui/lib/compliance/compliance-provider-filters.test.ts b/ui/lib/compliance/compliance-provider-filters.test.ts new file mode 100644 index 0000000000..43e8de3e12 --- /dev/null +++ b/ui/lib/compliance/compliance-provider-filters.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest"; + +import { + COMPLIANCE_PROVIDER_FILTER_KEYS, + extractComplianceProviderFilters, + hasComplianceProviderFilters, +} from "./compliance-provider-filters"; + +describe("COMPLIANCE_PROVIDER_FILTER_KEYS", () => { + it("contains the three provider scope keys", () => { + expect(COMPLIANCE_PROVIDER_FILTER_KEYS).toEqual([ + "filter[provider_type__in]", + "filter[provider_id__in]", + "filter[provider_groups__in]", + ]); + }); +}); + +describe("hasComplianceProviderFilters", () => { + for (const key of COMPLIANCE_PROVIDER_FILTER_KEYS) { + it(`is true when ${key} is present (plain object)`, () => { + expect(hasComplianceProviderFilters({ [key]: "abc" })).toBe(true); + }); + } + + it("is true when reading from URLSearchParams", () => { + const params = new URLSearchParams(); + params.set("filter[provider_groups__in]", "g1,g2"); + expect(hasComplianceProviderFilters(params)).toBe(true); + }); + + it("is false for scanId / region / unrelated params", () => { + expect( + hasComplianceProviderFilters({ + scanId: "scan-1", + "filter[region__in]": "eu-west-1", + }), + ).toBe(false); + }); + + it("treats empty / whitespace values as absent", () => { + expect( + hasComplianceProviderFilters({ + "filter[provider_type__in]": "", + "filter[provider_id__in]": " ", + }), + ).toBe(false); + }); + + it("is false for an empty object", () => { + expect(hasComplianceProviderFilters({})).toBe(false); + }); +}); + +describe("extractComplianceProviderFilters", () => { + it("returns only the present, non-empty provider keys", () => { + expect( + extractComplianceProviderFilters({ + "filter[provider_type__in]": "aws,gcp", + "filter[provider_id__in]": "", + scanId: "scan-1", + "filter[region__in]": "eu-west-1", + }), + ).toEqual({ "filter[provider_type__in]": "aws,gcp" }); + }); + + it("joins array values with commas", () => { + expect( + extractComplianceProviderFilters({ + "filter[provider_groups__in]": ["g1", "g2"], + }), + ).toEqual({ "filter[provider_groups__in]": "g1,g2" }); + }); + + it("reads from URLSearchParams", () => { + const params = new URLSearchParams(); + params.set("filter[provider_id__in]", "p1,p2"); + expect(extractComplianceProviderFilters(params)).toEqual({ + "filter[provider_id__in]": "p1,p2", + }); + }); + + it("returns an empty object when no provider filters are present", () => { + expect(extractComplianceProviderFilters({ scanId: "scan-1" })).toEqual({}); + }); +}); diff --git a/ui/lib/compliance/compliance-provider-filters.ts b/ui/lib/compliance/compliance-provider-filters.ts new file mode 100644 index 0000000000..ce0f1a25aa --- /dev/null +++ b/ui/lib/compliance/compliance-provider-filters.ts @@ -0,0 +1,46 @@ +import type { SearchParamsProps } from "@/types/components"; + +/** + * Provider-scope filter keys the compliance UI sets. A subset of the API's + * `PROVIDER_FILTER_KEYS`; the backend (`ComplianceOverviewViewSet`) treats these + * as an alternative to `filter[scan_id]` (XOR) and aggregates compliance across + * the latest completed scan of each matching provider. + */ +export const COMPLIANCE_PROVIDER_FILTER_KEYS = [ + "filter[provider_type__in]", + "filter[provider_id__in]", + "filter[provider_groups__in]", +] as const; + +type SearchParamsLike = SearchParamsProps | URLSearchParams; + +const readParam = (params: SearchParamsLike, key: string): string => { + if (params instanceof URLSearchParams) { + return params.get(key) ?? ""; + } + const value = params[key]; + if (Array.isArray(value)) return value.join(","); + return value ?? ""; +}; + +/** True when any compliance provider-scope filter is present and non-empty. */ +export const hasComplianceProviderFilters = ( + params: SearchParamsLike, +): boolean => + COMPLIANCE_PROVIDER_FILTER_KEYS.some( + (key) => readParam(params, key).trim().length > 0, + ); + +/** Returns only the present, non-empty provider-scope filters (CSV-joined). */ +export const extractComplianceProviderFilters = ( + params: SearchParamsLike, +): Record => { + const result: Record = {}; + for (const key of COMPLIANCE_PROVIDER_FILTER_KEYS) { + const value = readParam(params, key).trim(); + if (value.length > 0) { + result[key] = value; + } + } + return result; +};