Compare commits

...

12 Commits

Author SHA1 Message Date
sumit_chaturvedi
5cb9bb53e4 Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement
# Conflicts:
#	ui/components/ui/custom/custom-dropdown-filter.tsx
2025-06-06 12:47:32 +05:30
Pablo Lara
8a8586f298 chore: scan attributes are always available in compliance-scan-info component 2025-06-04 11:26:41 +02:00
Pablo Lara
e9a22c1513 Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement 2025-06-03 15:59:52 +02:00
Pablo Lara
5b22dcb7ac chore: remove unused types 2025-06-03 12:19:13 +02:00
sumit_chaturvedi
d8683630a9 srn Lint fix 2025-06-03 12:18:53 +05:30
sumit_chaturvedi
d2d68ded2b Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement
# Conflicts:
#	ui/CHANGELOG.md
#	ui/app/(prowler)/compliance/page.tsx
#	ui/types/components.ts
2025-06-03 12:17:29 +05:30
sumit_chaturvedi
9c19f9bc0c srn build fix 2025-06-02 09:03:31 +05:30
sumit_chaturvedi
c17001bb4f srn refactoring 2025-06-02 08:53:29 +05:30
sumit_chaturvedi
1dc8636276 Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement
# Conflicts:
#	ui/CHANGELOG.md
#	ui/components/ui/custom/custom-dropdown-filter.tsx
2025-06-02 08:33:33 +05:30
sumit_chaturvedi
97d216c49f Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement
# Conflicts:
#	ui/CHANGELOG.md
#	ui/app/(prowler)/findings/page.tsx
#	ui/components/ui/custom/custom-dropdown-filter.tsx
#	ui/types/filters.ts
2025-05-28 17:58:39 +05:30
sumit_chaturvedi
f87eaeac6b docs: changelog update 2025-05-23 13:39:29 +05:30
sumit_chaturvedi
ff995e7750 fix(ui): Improved the findings page scan Id filter 2025-05-23 13:17:26 +05:30
9 changed files with 141 additions and 55 deletions

View File

@@ -23,6 +23,7 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🔄 Changed
- Add `Provider UID` filter to scans page. [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820)
- Improve `Scan ID` filter by adding more context and enhancing the UI/UX. [(#7827)](https://github.com/prowler-cloud/prowler/pull/7827/)
---

View File

@@ -16,6 +16,7 @@ export const getScans = async ({
sort = "",
filters = {},
pageSize = 10,
include = "",
}) => {
const headers = await getAuthHeaders({ contentType: false });
@@ -27,6 +28,7 @@ export const getScans = async ({
if (pageSize) url.searchParams.append("page[size]", pageSize.toString());
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
if (include) url.searchParams.append("include", include);
// Handle multiple filters
Object.entries(filters).forEach(([key, value]) => {

View File

@@ -3,7 +3,6 @@ import { Suspense } from "react";
import { getCompliancesOverview } from "@/actions/compliances";
import { getComplianceOverviewMetadataInfo } from "@/actions/compliances";
import { getProvider } from "@/actions/providers";
import { getScans } from "@/actions/scans";
import {
ComplianceCard,
@@ -12,6 +11,7 @@ import {
} from "@/components/compliance";
import { ComplianceHeader } from "@/components/compliance/compliance-header/compliance-header";
import { ContentLayout } from "@/components/ui";
import { getProviderDetailsByScan } from "@/lib/provider-helpers";
import { ScanProps, SearchParamsProps } from "@/types";
import { ComplianceOverviewData } from "@/types/compliance";
@@ -31,6 +31,7 @@ export default async function Compliance({
"filter[state]": "completed",
},
pageSize: 50,
include: "provider",
});
if (!scansData?.data) {
@@ -38,31 +39,10 @@ export default async function Compliance({
}
// Expand scans with provider information
const expandedScansData = await Promise.all(
scansData.data.map(async (scan: ScanProps) => {
const providerId = scan.relationships?.provider?.data?.id;
if (!providerId) {
return { ...scan, providerInfo: null };
}
const formData = new FormData();
formData.append("id", providerId);
const providerData = await getProvider(formData);
return {
...scan,
providerInfo: providerData?.data
? {
provider: providerData.data.attributes.provider,
uid: providerData.data.attributes.uid,
alias: providerData.data.attributes.alias,
}
: null,
};
}),
);
const expandedScansData = scansData.data.map((scan: ScanProps) => ({
id: scan.id,
...getProviderDetailsByScan(scan, scansData.included, "flat"),
}));
const selectedScanId =
searchParams.scanId || expandedScansData[0]?.id || null;

View File

@@ -26,6 +26,7 @@ import {
import {
createProviderDetailsMapping,
extractProviderUIDs,
getProviderDetailsByScan,
} from "@/lib/provider-helpers";
import { ScanProps } from "@/types";
import { FindingProps, SearchParamsProps } from "@/types/components";
@@ -48,7 +49,9 @@ export default async function Findings({
filters,
}),
getProviders({ pageSize: 50 }),
getScans({}),
getScans({
include: "provider",
}),
]);
// Extract unique regions and services from the new endpoint
@@ -83,13 +86,17 @@ export default async function Findings({
scan.attributes.unique_resource_count > 1,
)
.map((scan: ScanProps) => ({
...scan,
id: scan.id,
name: scan.attributes.name,
}));
const completedScanIds =
completedScans?.map((scan: ScanProps) => scan.id) || [];
const providerDetailsAssociatedWithScans = scansData.data.map(
(scan: ScanProps) => getProviderDetailsByScan(scan, scansData.included),
);
return (
<ContentLayout title="Findings" icon="carbon:data-view-alt">
<FilterControls search date />
@@ -119,6 +126,7 @@ export default async function Findings({
key: "scan__in",
labelCheckboxGroup: "Scan ID",
values: completedScanIds,
valueLabelMapping: providerDetailsAssociatedWithScans,
index: 9,
},
]}

View File

@@ -11,7 +11,7 @@ interface ComplianceScanInfoProps {
alias?: string;
uid?: string;
};
attributes: {
attributes?: {
name?: string;
completed_at: string;
};
@@ -29,13 +29,17 @@ export const ComplianceScanInfo: React.FC<ComplianceScanInfoProps> = ({
entityId={scan.providerInfo.uid}
hideCopyButton
/>
<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>
<DateWithTime inline dateTime={scan.attributes.completed_at} />
</div>
{scan.attributes && (
<>
<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>
<DateWithTime inline dateTime={scan.attributes.completed_at} />
</div>
</>
)}
</div>
);
};

View File

@@ -20,7 +20,7 @@ import React, {
useState,
} from "react";
import { EntityInfoShort } from "@/components/ui/entities";
import { ComplianceScanInfo } from "@/components/compliance";
import { CustomDropdownFilterProps } from "@/types";
export const CustomDropdownFilter = ({
@@ -180,7 +180,7 @@ export const CustomDropdownFilter = ({
const entity = filter.valueLabelMapping?.find((entry) => entry[value])?.[
value
];
return entity?.alias || entity?.uid || value;
return entity?.providerInfo?.alias || entity?.providerInfo?.uid || value;
},
[filter.valueLabelMapping],
);
@@ -251,7 +251,7 @@ export const CustomDropdownFilter = ({
</div>
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 dark:bg-prowler-blue-800">
<PopoverContent className="min-w-[20rem] dark:bg-prowler-blue-800">
<div className="flex w-full flex-col gap-4 p-2">
<CheckboxGroup
color="default"
@@ -281,7 +281,8 @@ export const CustomDropdownFilter = ({
{filterValues.map((value) => {
const entity = filter.valueLabelMapping?.find(
(entry) => entry[value],
)?.[value];
);
const scanData = entity?.[value];
return (
<Checkbox
@@ -292,13 +293,8 @@ export const CustomDropdownFilter = ({
key={value}
value={value}
>
{entity ? (
<EntityInfoShort
cloudProvider={entity.provider}
entityAlias={entity.alias ?? undefined}
entityId={entity.uid}
hideCopyButton
/>
{scanData ? (
<ComplianceScanInfo scan={scanData} />
) : (
value
)}

View File

@@ -20,7 +20,7 @@ export const EntityInfoShort: React.FC<EntityInfoProps> = ({
hideCopyButton = false,
}) => {
return (
<div className="flex w-full items-center justify-between space-x-2">
<div className="flex w-fit items-center justify-between">
<div className="flex items-center gap-x-2">
<div className="flex-shrink-0">{getProviderLogo(cloudProvider)}</div>
<div className="flex flex-col">

View File

@@ -1,7 +1,8 @@
import { ScanProps } from "@/types";
import {
ProviderAccountProps,
ProviderProps,
ProvidersApiResponse,
ProviderType,
} from "@/types/providers";
export const extractProviderUIDs = (
@@ -21,7 +22,15 @@ export const extractProviderUIDs = (
export const createProviderDetailsMapping = (
providerUIDs: string[],
providersData: ProvidersApiResponse,
): Array<{ [uid: string]: ProviderAccountProps }> => {
): Array<{
[uid: string]: {
providerInfo: {
provider: ProviderType;
alias?: string;
uid?: string;
};
};
}> => {
if (!providersData?.data) return [];
return providerUIDs.map((uid) => {
@@ -31,10 +40,84 @@ export const createProviderDetailsMapping = (
return {
[uid]: {
provider: provider?.attributes?.provider || "aws",
uid: uid,
alias: provider?.attributes?.alias ?? null,
providerInfo: {
provider: provider?.attributes?.provider || "aws",
uid: uid,
alias: provider?.attributes?.alias,
},
},
};
});
};
/**
* Extracts and formats provider details associated with a given scan.
*
* @param scan - The scan object containing scan attributes and provider relationship data.
* @param included - An array of included related resources (e.g., providers).
* @param format - Optional. Specifies the structure of the returned object.
* - "keyed" (default): Returns an object keyed by scan ID with providerInfo and attributes.
* - "flat": Returns a flat object containing providerInfo and attributes.
* - "merged": Returns the full scan object with providerInfo merged in.
*
* @returns An object containing provider information and scan attributes in the specified format.
*
* @example
* const result = getProviderDetailsByScan(scan, included, "flat");
* // {
* // providerInfo: { provider: 'Example Provider', alias: 'EP', uid: '123' },
* // attributes: { name: 'Scan 1', started_at: '2024-01-01', completed_at: '2024-01-02' }
* // }
*/
export const getProviderDetailsByScan = (
scan: ScanProps,
included: ProviderProps[],
format: "keyed" | "flat" | "merged" = "keyed",
) => {
const providerId = scan.relationships?.provider?.data?.id;
const providerDetails = providerId
? included.find(
(provider) =>
provider.type === "providers" && provider.id === providerId,
)
: null;
const providerInfo = {
provider: providerDetails?.attributes?.provider,
alias: providerDetails?.attributes?.alias ?? null,
uid: providerDetails?.attributes?.uid ?? null,
};
const attributes = {
name: scan.attributes?.name,
started_at: scan.attributes?.started_at,
completed_at: scan.attributes?.completed_at,
};
// --- FORMAT 1: keyed (default)
if (format === "keyed") {
return {
[scan.id]: {
providerInfo,
attributes,
},
};
}
// --- FORMAT 2: flat object
if (format === "flat") {
return {
providerInfo,
attributes,
};
}
// --- FORMAT 3: merged into full scan object
if (format === "merged") {
return {
...scan,
providerInfo,
};
}
};

View File

@@ -1,10 +1,22 @@
import { ProviderAccountProps } from "./providers";
import { ProviderType } from "./providers";
export interface FilterOption {
key: string;
labelCheckboxGroup: string;
values: string[];
valueLabelMapping?: Array<{ [uid: string]: ProviderAccountProps }>;
valueLabelMapping?: Array<{
[uid: string]: {
providerInfo: {
provider: ProviderType;
alias?: string;
uid?: string;
};
attributes?: {
name?: string;
completed_at: string;
};
};
}>;
index?: number;
showSelectAll?: boolean;
defaultToSelectAll?: boolean;