feat: scan id filter drowpdown (#7949)

Co-authored-by: Pablo Lara <larabjj@gmail.com>
This commit is contained in:
Alejandro Bailo
2025-06-06 12:38:14 +02:00
committed by GitHub
parent acf333493a
commit 50dc396aa3
16 changed files with 176 additions and 55 deletions

View File

@@ -19,6 +19,7 @@ import { ContentLayout } from "@/components/ui";
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
import {
createDict,
createScanDetailsMapping,
extractFiltersAndQuery,
extractSortAndKey,
hasDateOrScanFilter,
@@ -48,7 +49,7 @@ export default async function Findings({
filters,
}),
getProviders({ pageSize: 50 }),
getScans({}),
getScans({ pageSize: 50 }),
]);
// Extract unique regions and services from the new endpoint
@@ -76,20 +77,17 @@ export default async function Findings({
});
// Extract scan UUIDs with "completed" state and more than one resource
const completedScans = scansData?.data
?.filter(
(scan: ScanProps) =>
scan.attributes.state === "completed" &&
scan.attributes.unique_resource_count > 1,
)
.map((scan: ScanProps) => ({
id: scan.id,
name: scan.attributes.name,
}));
const completedScans = scansData?.data?.filter(
(scan: ScanProps) =>
scan.attributes.state === "completed" &&
scan.attributes.unique_resource_count > 1,
);
const completedScanIds =
completedScans?.map((scan: ScanProps) => scan.id) || [];
const scanDetails = createScanDetailsMapping(completedScans, providersData);
return (
<ContentLayout title="Findings" icon="carbon:data-view-alt">
<FilterControls search date />
@@ -119,6 +117,7 @@ export default async function Findings({
key: "scan__in",
labelCheckboxGroup: "Scan ID",
values: completedScanIds,
valueLabelMapping: scanDetails,
index: 9,
},
]}

View File

@@ -6,7 +6,7 @@ import { FilterControls } from "@/components/filters";
import { DataTableFilterCustom } from "@/components/ui/table/data-table-filter-custom";
import { DataCompliance } from "./data-compliance";
import { SelectScanComplianceDataProps } from "./select-scan-compliance-data";
import { SelectScanComplianceDataProps } from "./scan-selector";
interface ComplianceHeaderProps {
scans: SelectScanComplianceDataProps["scans"];

View File

@@ -1,4 +1,4 @@
import { Divider } from "@nextui-org/react";
import { Divider, Tooltip } from "@nextui-org/react";
import React from "react";
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
@@ -22,18 +22,25 @@ export const ComplianceScanInfo: React.FC<ComplianceScanInfoProps> = ({
scan,
}) => {
return (
<div className="flex w-fit items-center">
<div className="flex items-center gap-2">
<EntityInfoShort
cloudProvider={scan.providerInfo.provider}
entityAlias={scan.providerInfo.alias}
entityId={scan.providerInfo.uid}
hideCopyButton
snippetWidth="max-w-[100px]"
/>
<Divider orientation="vertical" className="mx-2 h-6" />
<div className="flex flex-col items-start">
<p className="text-xs text-default-500">
{scan.attributes.name || "- -"}
</p>
<Divider orientation="vertical" className="h-6" />
<div className="flex flex-col items-start whitespace-nowrap">
<Tooltip
content={scan.attributes.name || "- -"}
placement="top"
size="sm"
>
<p className="text-xs text-default-500">
{scan.attributes.name || "- -"}
</p>
</Tooltip>
<DateWithTime inline dateTime={scan.attributes.completed_at} />
</div>
</div>

View File

@@ -4,7 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import {
SelectScanComplianceData,
ScanSelector,
SelectScanComplianceDataProps,
} from "@/components/compliance/compliance-header/index";
interface DataComplianceProps {
@@ -35,8 +35,8 @@ export const DataCompliance = ({ scans }: DataComplianceProps) => {
return (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 lg:grid-cols-3">
<SelectScanComplianceData
<div className="flex max-w-fit">
<ScanSelector
scans={scans}
selectedScanId={selectedScanId}
onSelectionChange={handleScanChange}

View File

@@ -1,2 +1,2 @@
export * from "./data-compliance";
export * from "./select-scan-compliance-data";
export * from "./scan-selector";

View File

@@ -16,7 +16,7 @@ export interface SelectScanComplianceDataProps {
onSelectionChange: (selectedKey: string) => void;
}
export const SelectScanComplianceData = ({
export const ScanSelector = ({
scans,
selectedScanId,
onSelectionChange,
@@ -26,7 +26,7 @@ export const SelectScanComplianceData = ({
aria-label="Select a Scan"
placeholder="Select a scan"
classNames={{
selectorIcon: "right-2",
trigger: "w-full min-w-[365px]",
}}
size="lg"
labelPlacement="outside"

View File

@@ -12,7 +12,7 @@ export * from "./compliance-custom-details/iso-details";
export * from "./compliance-header/compliance-header";
export * from "./compliance-header/compliance-scan-info";
export * from "./compliance-header/data-compliance";
export * from "./compliance-header/select-scan-compliance-data";
export * from "./compliance-header/scan-selector";
export * from "./no-scans-available";
export * from "./skeletons/bar-chart-skeleton";
export * from "./skeletons/compliance-accordion-skeleton";

View File

@@ -20,8 +20,15 @@ import React, {
useState,
} from "react";
import { ComplianceScanInfo } from "@/components/compliance/compliance-header/compliance-scan-info";
import { EntityInfoShort } from "@/components/ui/entities";
import { CustomDropdownFilterProps } from "@/types";
import { isScanEntity } from "@/lib/helper-filters";
import {
CustomDropdownFilterProps,
FilterEntity,
ProviderEntity,
ScanEntity,
} from "@/types";
export const CustomDropdownFilter = ({
filter,
@@ -177,10 +184,25 @@ export const CustomDropdownFilter = ({
const getDisplayLabel = useCallback(
(value: string) => {
const entity = filter.valueLabelMapping?.find((entry) => entry[value])?.[
value
];
return entity?.alias || entity?.uid || value;
const entity: FilterEntity | undefined = filter.valueLabelMapping?.find(
(entry) => entry[value],
)?.[value];
if (!entity) return value;
if (isScanEntity(entity as ScanEntity)) {
return (
(entity as ScanEntity).attributes?.name ||
(entity as ScanEntity).providerInfo?.alias ||
(entity as ScanEntity).providerInfo?.uid ||
value
);
} else {
return (
(entity as ProviderEntity).alias ||
(entity as ProviderEntity).uid ||
value
);
}
},
[filter.valueLabelMapping],
);
@@ -239,7 +261,7 @@ export const CustomDropdownFilter = ({
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClearAll(e as any);
handleClearAll(e as unknown as React.MouseEvent);
}
}}
>
@@ -251,7 +273,7 @@ export const CustomDropdownFilter = ({
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 dark:bg-prowler-blue-800">
<PopoverContent className="w-auto min-w-80 dark:bg-prowler-blue-800">
<div className="flex w-full flex-col gap-4 p-2">
<CheckboxGroup
color="default"
@@ -279,9 +301,10 @@ export const CustomDropdownFilter = ({
className="flex max-h-96 max-w-full flex-col gap-y-2 py-2"
>
{filterValues.map((value) => {
const entity = filter.valueLabelMapping?.find(
(entry) => entry[value],
)?.[value];
const entity: FilterEntity | undefined =
filter.valueLabelMapping?.find((entry) => entry[value])?.[
value
];
return (
<Checkbox
@@ -293,12 +316,18 @@ export const CustomDropdownFilter = ({
value={value}
>
{entity ? (
<EntityInfoShort
cloudProvider={entity.provider}
entityAlias={entity.alias ?? undefined}
entityId={entity.uid}
hideCopyButton
/>
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
)}

View File

@@ -30,9 +30,13 @@ export const DateWithTime: React.FC<DateWithTimeProps> = ({
<div
className={`flex ${inline ? "flex-row items-center gap-2" : "flex-col"}`}
>
<span className="text-xs font-semibold">{formattedDate}</span>
<span className="whitespace-nowrap text-xs font-semibold">
{formattedDate}
</span>
{showTime && (
<span className="text-xs text-gray-500">{formattedTime}</span>
<span className="whitespace-nowrap text-xs text-gray-500">
{formattedTime}
</span>
)}
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { Tooltip } from "@nextui-org/react";
import React from "react";
import { IdIcon } from "@/components/icons";
@@ -11,6 +12,7 @@ interface EntityInfoProps {
entityAlias?: string;
entityId?: string;
hideCopyButton?: boolean;
snippetWidth?: string;
}
export const EntityInfoShort: React.FC<EntityInfoProps> = ({
@@ -20,12 +22,16 @@ export const EntityInfoShort: React.FC<EntityInfoProps> = ({
hideCopyButton = false,
}) => {
return (
<div className="flex w-full items-center justify-between space-x-2">
<div className="flex items-center gap-x-2">
<div className="flex items-center justify-start">
<div className="flex items-center justify-between gap-x-2">
<div className="flex-shrink-0">{getProviderLogo(cloudProvider)}</div>
<div className="flex flex-col">
<div className="flex max-w-[120px] flex-col">
{entityAlias && (
<span className="text-xs text-default-500">{entityAlias}</span>
<Tooltip content={entityAlias} placement="top" size="sm">
<span className="truncate text-ellipsis text-xs text-default-500">
{entityAlias}
</span>
</Tooltip>
)}
<SnippetChip
value={entityId ?? ""}

View File

@@ -23,6 +23,10 @@ export const SnippetChip = ({
return (
<Snippet
className={cn("h-6", className)}
classNames={{
content: "min-w-0 overflow-hidden",
pre: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
}}
color="default"
size="sm"
variant="flat"
@@ -34,10 +38,13 @@ export const SnippetChip = ({
codeString={value}
{...props}
>
<div className="flex items-center space-x-2" aria-label={ariaLabel}>
<div
className="flex min-w-0 items-center space-x-2"
aria-label={ariaLabel}
>
{icon}
<Tooltip content={value} placement="top" size="sm">
<span className="no-scrollbar max-w-24 overflow-hidden overflow-x-scroll text-ellipsis whitespace-nowrap text-xs">
<span className="min-w-0 flex-1 truncate text-xs">
{formatter ? formatter(value) : value}
</span>
</Tooltip>

View File

@@ -1,3 +1,6 @@
import { ProviderProps, ProvidersApiResponse, ScanProps } from "@/types";
import { ScanEntity } from "@/types/scans";
/**
* Extracts normalized filters and search query from the URL search params.
* Used Server Side Rendering (SSR). There is a hook (useUrlFilters) for client side.
@@ -44,3 +47,54 @@ export const extractSortAndKey = (searchParams: Record<string, unknown>) => {
return { searchParamsKey, rawSort, encodedSort };
};
export const isScanEntity = (entity: ScanEntity) => {
return entity && entity.providerInfo && entity.attributes;
};
/**
* Creates a scan details mapping for filters from completed scans.
* Used to provide detailed information for scan filters in the UI.
*/
export const createScanDetailsMapping = (
completedScans: ScanProps[],
providersData?: ProvidersApiResponse,
) => {
if (!completedScans || completedScans.length === 0) {
return [];
}
const scanMappings = completedScans.map((scan: ScanProps) => {
// Get provider info from providerInfo if available, or find from providers data
let providerInfo = scan.providerInfo;
if (!providerInfo && scan.relationships?.provider?.data?.id) {
const provider = providersData?.data?.find(
(p: ProviderProps) => p.id === scan.relationships.provider.data.id,
);
if (provider) {
providerInfo = {
provider: provider.attributes.provider,
alias: provider.attributes.alias,
uid: provider.attributes.uid,
};
}
}
return {
[scan.id]: {
providerInfo: {
provider: providerInfo?.provider || "aws",
alias: providerInfo?.alias,
uid: providerInfo?.uid,
},
attributes: {
name: scan.attributes.name,
completed_at: scan.attributes.completed_at,
},
},
};
});
return scanMappings;
};

View File

@@ -1,5 +1,5 @@
import {
ProviderAccountProps,
ProviderEntity,
ProviderProps,
ProvidersApiResponse,
} from "@/types/providers";
@@ -21,7 +21,7 @@ export const extractProviderUIDs = (
export const createProviderDetailsMapping = (
providerUIDs: string[],
providersData: ProvidersApiResponse,
): Array<{ [uid: string]: ProviderAccountProps }> => {
): Array<{ [uid: string]: ProviderEntity }> => {
if (!providersData?.data) return [];
return providerUIDs.map((uid) => {

View File

@@ -1,10 +1,13 @@
import { ProviderAccountProps } from "./providers";
import { ProviderEntity } from "./providers";
import { ScanEntity } from "./scans";
export type FilterEntity = ProviderEntity | ScanEntity;
export interface FilterOption {
key: string;
labelCheckboxGroup: string;
values: string[];
valueLabelMapping?: Array<{ [uid: string]: ProviderAccountProps }>;
valueLabelMapping?: Array<{ [uid: string]: FilterEntity }>;
index?: number;
showSelectAll?: boolean;
defaultToSelectAll?: boolean;

View File

@@ -45,7 +45,7 @@ export interface ProviderProps {
groupNames?: string[];
}
export interface ProviderAccountProps {
export interface ProviderEntity {
provider: ProviderType;
uid: string;
alias: string | null;

View File

@@ -47,3 +47,15 @@ export interface ScanProps {
alias: string;
};
}
export interface ScanEntity {
providerInfo: {
provider: ProviderType;
alias?: string;
uid?: string;
};
attributes: {
name?: string;
completed_at: string;
};
}