feat: filters relationships in findings and scans page (#8046)

Co-authored-by: Pablo Lara <larabjj@gmail.com>
This commit is contained in:
Alejandro BailoandPablo Lara authored and GitHub committed 2025-06-18 17:19:41 +02:00
1 parent b3f2a1c532
commit 6b7b700a98
22 files changed
+576 -265

No files matched your search

@@ -58,11 +58,10 @@ export const ComplianceHeader = ({
<>
{(showProviders || showSearch) && (
<>
<div className="flex items-center justify-start gap-4">
<div className="flex items-start justify-start gap-4">
{showProviders && <DataCompliance scans={scans} />}
{showSearch && <FilterControls search />}
</div>
<Spacer y={8} />
</>
)}
{allFilters.length > 0 && (
@@ -0,0 +1,38 @@
"use client";
import { CrossIcon } from "@/components/icons";
import { useUrlFilters } from "@/hooks/use-url-filters";
import { CustomButton } from "../ui/custom/custom-button";
export interface ClearFiltersButtonProps {
className?: string;
text?: string;
ariaLabel?: string;
}
export const ClearFiltersButton = ({
className = "w-full md:w-fit",
text = "Clear all filters",
ariaLabel = "Reset",
}: ClearFiltersButtonProps) => {
const { clearAllFilters, hasFilters } = useUrlFilters();
if (!hasFilters()) {
return null;
}
return (
<CustomButton
ariaLabel={ariaLabel}
className={className}
onPress={clearAllFilters}
variant="dashed"
size="md"
endContent={<CrossIcon size={24} />}
radius="sm"
>
{text}
</CustomButton>
);
};
@@ -74,7 +74,7 @@ export const CustomSelectProvider: React.FC = () => {
placeholder="Select a provider"
classNames={{
selectorIcon: "right-2",
label: "!z-0",
label: "!z-0 mb-2",
}}
label="Provider"
labelPlacement="inside"
+17 -23
View File
@@ -1,3 +1,5 @@
import { FilterType } from "@/types/filters";
export const filterProviders = [
{
key: "connected",
@@ -12,6 +14,7 @@ export const filterScans = [
key: "provider_type__in",
labelCheckboxGroup: "Cloud Provider",
values: ["aws", "azure", "m365", "gcp", "kubernetes"],
index: 0,
},
{
key: "state__in",
@@ -24,52 +27,43 @@ export const filterScans = [
"failed",
"cancelled",
],
index: 2,
},
{
key: "trigger",
labelCheckboxGroup: "Trigger",
values: ["scheduled", "manual"],
},
{
key: "provider_uid__in",
labelCheckboxGroup: "Provider UID",
values: [],
index: 3,
},
// Add more filter categories as needed
];
//Static filters for findings
export const filterFindings = [
{
key: "severity__in",
key: FilterType.SEVERITY,
labelCheckboxGroup: "Severity",
values: ["critical", "high", "medium", "low", "informational"],
index: 0,
},
{
key: FilterType.STATUS,
labelCheckboxGroup: "Status",
values: ["PASS", "FAIL", "MANUAL"],
index: 1,
},
{
key: "status__in",
labelCheckboxGroup: "Status",
values: ["PASS", "FAIL", "MANUAL"],
index: 2,
},
{
key: "provider_type__in",
key: FilterType.PROVIDER_TYPE,
labelCheckboxGroup: "Cloud Provider",
values: ["aws", "azure", "m365", "gcp", "kubernetes"],
index: 4,
index: 5,
},
{
key: "provider_uid__in",
labelCheckboxGroup: "Provider UID",
values: [],
index: 8,
},
{
key: "delta__in",
key: FilterType.DELTA,
labelCheckboxGroup: "Delta",
values: ["new", "changed"],
index: 3,
index: 2,
},
// Add more filter categories as needed
];
export const filterUsers = [
+12 -20
View File
@@ -1,14 +1,13 @@
"use client";
import { Spacer } from "@nextui-org/react";
import { useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { useUrlFilters } from "@/hooks/use-url-filters";
import { FilterControlsProps } from "@/types";
import { CrossIcon } from "../icons";
import { CustomButton } from "../ui/custom";
import { DataTableFilterCustom } from "../ui/table";
import { ClearFiltersButton } from "./clear-filters-button";
import { CustomAccountSelection } from "./custom-account-selection";
import { CustomCheckboxMutedFindings } from "./custom-checkbox-muted-findings";
import { CustomDatePicker } from "./custom-date-picker";
@@ -26,7 +25,6 @@ export const FilterControls: React.FC<FilterControlsProps> = ({
customFilters,
}) => {
const searchParams = useSearchParams();
const { clearAllFilters } = useUrlFilters();
const [showClearButton, setShowClearButton] = useState(false);
useEffect(() => {
@@ -37,7 +35,7 @@ export const FilterControls: React.FC<FilterControlsProps> = ({
}, [searchParams]);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col">
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
{search && <CustomSearchInput />}
{providers && <CustomSelectProvider />}
@@ -45,22 +43,16 @@ export const FilterControls: React.FC<FilterControlsProps> = ({
{regions && <CustomRegionSelection />}
{accounts && <CustomAccountSelection />}
{mutedFindings && <CustomCheckboxMutedFindings />}
{showClearButton && (
<CustomButton
ariaLabel="Reset"
className="w-full md:w-fit"
onPress={clearAllFilters}
variant="dashed"
size="md"
endContent={<CrossIcon size={24} />}
radius="sm"
>
Clear all filters
</CustomButton>
)}
{!customFilters && showClearButton && <ClearFiltersButton />}
</div>
{customFilters && <DataTableFilterCustom filters={customFilters} />}
<Spacer y={8} />
{customFilters && (
<DataTableFilterCustom
filters={customFilters}
showClearButton={showClearButton}
defaultOpen
/>
)}
</div>
);
};
+1
View File
@@ -1,3 +1,4 @@
export * from "./clear-filters-button";
export * from "./custom-account-selection";
export * from "./custom-checkbox-muted-findings";
export * from "./custom-date-picker";
@@ -0,0 +1,79 @@
"use client";
import { filterFindings } from "@/components/filters/data-filters";
import { FilterControls } from "@/components/filters/filter-controls";
import { useRelatedFilters } from "@/hooks";
import { FilterEntity, FilterType, ScanEntity, ScanProps } from "@/types";
interface FindingsFiltersProps {
providerUIDs: string[];
providerDetails: { [uid: string]: FilterEntity }[];
completedScans: ScanProps[];
completedScanIds: string[];
scanDetails: { [key: string]: ScanEntity }[];
uniqueRegions: string[];
uniqueServices: string[];
uniqueResourceTypes: string[];
}
export const FindingsFilters = ({
providerUIDs,
providerDetails,
completedScanIds,
scanDetails,
uniqueRegions,
uniqueServices,
uniqueResourceTypes,
}: FindingsFiltersProps) => {
const { availableProviderUIDs, availableScans } = useRelatedFilters({
providerUIDs,
providerDetails,
completedScanIds,
scanDetails,
enableScanRelation: true,
});
return (
<>
<FilterControls
search
date
customFilters={[
...filterFindings,
{
key: FilterType.PROVIDER_UID,
labelCheckboxGroup: "Provider UID",
values: availableProviderUIDs,
valueLabelMapping: providerDetails,
index: 6,
},
{
key: FilterType.REGION,
labelCheckboxGroup: "Regions",
values: uniqueRegions,
index: 3,
},
{
key: FilterType.SERVICE,
labelCheckboxGroup: "Services",
values: uniqueServices,
index: 4,
},
{
key: FilterType.RESOURCE_TYPE,
labelCheckboxGroup: "Resource Type",
values: uniqueResourceTypes,
index: 8,
},
{
key: FilterType.SCAN,
labelCheckboxGroup: "Scan ID",
values: availableScans,
valueLabelMapping: scanDetails,
index: 7,
},
]}
/>
</>
);
};
+5 -15
View File
@@ -50,21 +50,11 @@ export const ProviderInfo: React.FC<ProviderInfoProps> = ({
};
return (
<div className="dark:bg-prowler-blue-400">
<div className="grid grid-cols-1">
<div className="flex items-center text-sm">
<div className="flex items-center">
<span className="flex items-center justify-center px-4">
{getProviderLogo(provider)}
</span>
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">{getIcon()}</div>
<span className="font-medium">
{providerAlias || providerUID}
</span>
</div>
</div>
</div>
<div className="flex items-center text-sm">
<div className="flex items-center space-x-4">
{getProviderLogo(provider)}
{getIcon()}
<span className="font-medium">{providerAlias || providerUID}</span>
</div>
</div>
);
+1
View File
@@ -2,3 +2,4 @@ export * from "./auto-refresh";
export * from "./link-to-findings-from-scan";
export * from "./no-providers-added";
export * from "./no-providers-connected";
export * from "./scans-filters";
+1 -1
View File
@@ -8,7 +8,7 @@ import { CustomButton } from "../ui/custom";
export const NoProvidersAdded = () => {
return (
<div className="flex min-h-screen items-center justify-center dark:bg-prowler-blue-800">
<div className="flex min-h-screen items-center justify-center">
<div className="mx-auto w-full max-w-7xl px-4">
<Card className="mx-auto w-full max-w-3xl rounded-lg dark:bg-prowler-blue-400">
<CardBody className="flex flex-col items-center space-y-4 p-6 text-center sm:p-8">
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { filterScans } from "@/components/filters/data-filters";
import { FilterControls } from "@/components/filters/filter-controls";
import { useRelatedFilters } from "@/hooks";
import { FilterEntity, FilterType } from "@/types";
interface ScansFiltersProps {
providerUIDs: string[];
providerDetails: { [uid: string]: FilterEntity }[];
}
export const ScansFilters = ({
providerUIDs,
providerDetails,
}: ScansFiltersProps) => {
const { availableProviderUIDs } = useRelatedFilters({
providerUIDs,
providerDetails,
enableScanRelation: false,
});
return (
<FilterControls
customFilters={[
...filterScans,
{
key: FilterType.PROVIDER_UID,
labelCheckboxGroup: "Provider UID",
values: availableProviderUIDs,
valueLabelMapping: providerDetails,
index: 1,
},
]}
/>
);
};
@@ -282,7 +282,10 @@ export const CustomDropdownFilter = ({
onValueChange={onSelectionChange}
className="font-bold"
>
{filter?.showSelectAll !== false && (
{filterValues.length === 0 && (
<span className="text-small font-normal">No results found</span>
)}
{filter?.showSelectAll !== false && filterValues.length > 0 && (
<>
<Checkbox
classNames={{
@@ -296,45 +299,49 @@ export const CustomDropdownFilter = ({
<Divider orientation="horizontal" className="mt-2" />
</>
)}
<ScrollShadow
hideScrollBar
className="flex max-h-96 max-w-full flex-col gap-y-2 py-2"
>
{filterValues.map((value) => {
const entity: FilterEntity | undefined =
filter.valueLabelMapping?.find((entry) => entry[value])?.[
value
];
return (
<Checkbox
classNames={{
label: "text-small font-normal",
wrapper: "checkbox-update",
}}
key={value}
value={value}
>
{entity ? (
isScanEntity(entity as ScanEntity) ? (
<ComplianceScanInfo scan={entity as ScanEntity} />
) : (
<EntityInfoShort
cloudProvider={(entity as ProviderEntity).provider}
entityAlias={
(entity as ProviderEntity).alias ?? undefined
}
entityId={(entity as ProviderEntity).uid}
hideCopyButton
/>
)
) : (
{filterValues.length > 0 && (
<ScrollShadow
hideScrollBar
className="flex max-h-96 max-w-full flex-col gap-y-2 py-2"
>
{filterValues.map((value) => {
const entity: FilterEntity | undefined =
filter.valueLabelMapping?.find((entry) => entry[value])?.[
value
)}
</Checkbox>
);
})}
</ScrollShadow>
];
return (
<Checkbox
classNames={{
label: "text-small font-normal",
wrapper: "checkbox-update",
}}
key={value}
value={value}
>
{entity ? (
isScanEntity(entity as ScanEntity) ? (
<ComplianceScanInfo scan={entity as ScanEntity} />
) : (
<EntityInfoShort
cloudProvider={
(entity as ProviderEntity).provider
}
entityAlias={
(entity as ProviderEntity).alias ?? undefined
}
entityId={(entity as ProviderEntity).uid}
hideCopyButton
/>
)
) : (
value
)}
</Checkbox>
);
})}
</ScrollShadow>
)}
</CheckboxGroup>
</div>
</PopoverContent>
@@ -3,6 +3,7 @@
import React, { useState } from "react";
import { useCallback, useMemo } from "react";
import { ClearFiltersButton } from "@/components/filters";
import { CustomFilterIcon } from "@/components/icons";
import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom";
import { useUrlFilters } from "@/hooks/use-url-filters";
@@ -11,11 +12,13 @@ import { FilterOption } from "@/types";
export interface DataTableFilterCustomProps {
filters: FilterOption[];
defaultOpen?: boolean;
showClearButton?: boolean;
}
export const DataTableFilterCustom = ({
filters,
defaultOpen = false,
showClearButton = false,
}: DataTableFilterCustomProps) => {
const { updateFilter } = useUrlFilters();
const [showFilters, setShowFilters] = useState(defaultOpen);
@@ -43,39 +46,33 @@ export const DataTableFilterCustom = ({
);
return (
<div
className={`flex ${
sortedFilters.length > 4 ? "flex-col" : "flex-col md:flex-row"
} gap-4`}
>
<CustomButton
ariaLabel={showFilters ? "Hide Filters" : "Show Filters"}
variant="flat"
color={showFilters ? "action" : "primary"}
size="md"
startContent={<CustomFilterIcon size={16} />}
onPress={() => setShowFilters(!showFilters)}
className="w-full max-w-fit"
>
<h3 className="text-small">
{showFilters ? "Hide Filters" : "Show Filters"}
</h3>
</CustomButton>
<div className="flex flex-col gap-4">
<div className="flex flex-row gap-4">
<CustomButton
ariaLabel={showFilters ? "Hide Filters" : "Show Filters"}
variant="flat"
color={showFilters ? "action" : "primary"}
size="md"
startContent={<CustomFilterIcon size={16} />}
onPress={() => setShowFilters(!showFilters)}
className="w-full max-w-fit"
>
<h3 className="text-small">
{showFilters ? "Hide Filters" : "Show Filters"}
</h3>
</CustomButton>
{showClearButton && <ClearFiltersButton />}
</div>
<div
className={`transition-all duration-700 ease-in-out ${
showFilters
? "max-h-96 w-full translate-x-0 overflow-visible opacity-100"
: "max-h-0 -translate-x-full overflow-hidden opacity-0"
? "w-full translate-x-0 overflow-visible opacity-100"
: "mt-[-16px] max-h-0 -translate-x-full overflow-hidden opacity-0"
}`}
>
<div
className={`grid gap-4 ${
sortedFilters.length >= 4
? "grid-cols-1 md:grid-cols-4"
: "grid-cols-1 md:grid-cols-3"
}`}
>
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
{sortedFilters.map((filter) => (
<CustomDropdownFilter
key={filter.key}