mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat: scan id filter drowpdown (#7949)
Co-authored-by: Pablo Lara <larabjj@gmail.com>
This commit is contained in:
@@ -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,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./data-compliance";
|
||||
export * from "./select-scan-compliance-data";
|
||||
export * from "./scan-selector";
|
||||
|
||||
@@ -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"
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?? ""}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface ProviderProps {
|
||||
groupNames?: string[];
|
||||
}
|
||||
|
||||
export interface ProviderAccountProps {
|
||||
export interface ProviderEntity {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string | null;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user