Skip to content

Commit b959584

Browse files
refactor(ui): improve compliance scan selector and download UX
1 parent 1b312fb commit b959584

12 files changed

Lines changed: 154 additions & 36 deletions

File tree

ui/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to the **Prowler UI** are documented in this file.
44

5+
## [1.24.1] (Prowler v5.24.1)
6+
7+
### 🐞 Fixed
8+
9+
- Compliance components coherence and minor UX fixes: simplified scan selector trigger to show a compact badge instead of full entity info, responsive mobile layout for compliance filters, and added download-started toast for CSV/PDF exports [(#10734)](https://github.com/prowler-cloud/prowler/pull/10734)
10+
11+
---
12+
513
## [1.24.0] (Prowler v5.24.0)
614

715
### 🚀 Added

ui/app/(prowler)/compliance/page.tsx

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Info } from "lucide-react";
12
import { Suspense } from "react";
23

34
import {
@@ -11,8 +12,9 @@ import {
1112
NoScansAvailable,
1213
ThreatScoreBadge,
1314
} from "@/components/compliance";
14-
import { ComplianceHeader } from "@/components/compliance/compliance-header/compliance-header";
15+
import { ComplianceFilters } from "@/components/compliance/compliance-header/compliance-filters";
1516
import { ComplianceOverviewGrid } from "@/components/compliance/compliance-overview-grid";
17+
import { Alert, AlertDescription } from "@/components/shadcn/alert";
1618
import { Card, CardContent } from "@/components/shadcn/card/card";
1719
import { ContentLayout } from "@/components/ui";
1820
import {
@@ -126,7 +128,7 @@ export default async function Compliance({
126128
<>
127129
{/* Row 1: Filters */}
128130
<div className="mb-6">
129-
<ComplianceHeader
131+
<ComplianceFilters
130132
scans={expandedScansData}
131133
uniqueRegions={uniqueRegions}
132134
/>
@@ -205,26 +207,23 @@ const SSRComplianceGrid = async ({
205207
type === "tasks"
206208
) {
207209
return (
208-
<ComplianceOverviewPanel>
209-
<div className="flex h-full min-h-40 items-center">
210-
<div className="text-text-neutral-secondary text-sm">
211-
No compliance data available for the selected scan.
212-
</div>
213-
</div>
214-
</ComplianceOverviewPanel>
210+
<Alert variant="info">
211+
<Info className="size-4" />
212+
<AlertDescription>
213+
This scan has no compliance data available yet, please select a
214+
different one.
215+
</AlertDescription>
216+
</Alert>
215217
);
216218
}
217219

218220
// Handle errors returned by the API
219221
if (compliancesData?.errors?.length > 0) {
220222
return (
221-
<ComplianceOverviewPanel>
222-
<div className="flex h-full min-h-40 items-center">
223-
<div className="text-text-neutral-secondary text-sm">
224-
Provide a valid scan ID.
225-
</div>
226-
</div>
227-
</ComplianceOverviewPanel>
223+
<Alert variant="info">
224+
<Info className="size-4" />
225+
<AlertDescription>Provide a valid scan ID.</AlertDescription>
226+
</Alert>
228227
);
229228
}
230229

@@ -250,9 +249,7 @@ const ComplianceOverviewPanel = ({
250249
padding="none"
251250
className="minimal-scrollbar shadow-small relative z-0 w-full gap-4 overflow-auto"
252251
>
253-
<CardContent className="flex flex-col gap-4 p-4">
254-
{children}
255-
</CardContent>
252+
<CardContent className="flex flex-col gap-4 p-4">{children}</CardContent>
256253
</Card>
257254
);
258255
};

ui/components/compliance/compliance-download-container.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { render, screen } from "@testing-library/react";
2-
import userEvent from "@testing-library/user-event";
31
import { readFileSync } from "node:fs";
42
import path from "node:path";
53
import { fileURLToPath } from "node:url";
4+
5+
import { render, screen } from "@testing-library/react";
6+
import userEvent from "@testing-library/user-event";
67
import { beforeEach, describe, expect, it, vi } from "vitest";
78

89
const { downloadComplianceCsvMock, downloadComplianceReportPdfMock } =
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use client";
2+
3+
import { useRouter, useSearchParams } from "next/navigation";
4+
import { useEffect } from "react";
5+
6+
import { ClearFiltersButton } from "@/components/filters/clear-filters-button";
7+
import {
8+
MultiSelect,
9+
MultiSelectContent,
10+
MultiSelectItem,
11+
MultiSelectSelectAll,
12+
MultiSelectSeparator,
13+
MultiSelectTrigger,
14+
MultiSelectValue,
15+
} from "@/components/shadcn/select/multiselect";
16+
import { useUrlFilters } from "@/hooks/use-url-filters";
17+
18+
import { ScanSelector, SelectScanComplianceDataProps } from "./scan-selector";
19+
20+
interface ComplianceFiltersProps {
21+
scans: SelectScanComplianceDataProps["scans"];
22+
uniqueRegions: string[];
23+
}
24+
25+
export const ComplianceFilters = ({
26+
scans,
27+
uniqueRegions,
28+
}: ComplianceFiltersProps) => {
29+
const router = useRouter();
30+
const searchParams = useSearchParams();
31+
const { updateFilter } = useUrlFilters();
32+
33+
const scanIdParam = searchParams.get("scanId");
34+
const selectedScanId = scanIdParam || (scans.length > 0 ? scans[0].id : "");
35+
36+
useEffect(() => {
37+
if (!scanIdParam && scans.length > 0) {
38+
const params = new URLSearchParams(searchParams);
39+
params.set("scanId", scans[0].id);
40+
router.replace(`?${params.toString()}`, { scroll: false });
41+
}
42+
}, [scans, scanIdParam, searchParams, router]);
43+
44+
const handleScanChange = (selectedKey: string) => {
45+
const params = new URLSearchParams(searchParams);
46+
params.set("scanId", selectedKey);
47+
router.push(`?${params.toString()}`, { scroll: false });
48+
};
49+
50+
const regionValues =
51+
searchParams.get("filter[region__in]")?.split(",").filter(Boolean) ?? [];
52+
53+
return (
54+
<div className="flex max-w-4xl flex-wrap items-center gap-4">
55+
<div className="w-full sm:max-w-[380px] sm:min-w-[200px] sm:flex-1">
56+
<ScanSelector
57+
scans={scans}
58+
selectedScanId={selectedScanId}
59+
onSelectionChange={handleScanChange}
60+
/>
61+
</div>
62+
{uniqueRegions.length > 0 && (
63+
<div className="w-full sm:max-w-[280px] sm:min-w-[200px] sm:flex-1">
64+
<MultiSelect
65+
values={regionValues}
66+
onValuesChange={(values) => updateFilter("region__in", values)}
67+
>
68+
<MultiSelectTrigger size="default">
69+
<MultiSelectValue placeholder="All Regions" />
70+
</MultiSelectTrigger>
71+
<MultiSelectContent search={false} width="wide">
72+
<MultiSelectSelectAll>Select All</MultiSelectSelectAll>
73+
<MultiSelectSeparator />
74+
{uniqueRegions.map((region) => (
75+
<MultiSelectItem key={region} value={region}>
76+
{region}
77+
</MultiSelectItem>
78+
))}
79+
</MultiSelectContent>
80+
</MultiSelect>
81+
</div>
82+
)}
83+
<ClearFiltersButton showCount />
84+
</div>
85+
);
86+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export * from "./compliance-filters";
12
export * from "./data-compliance";
23
export * from "./scan-selector";

ui/components/compliance/compliance-header/scan-selector.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { Badge } from "@/components/shadcn/badge/badge";
34
import {
45
Select,
56
SelectContent,
@@ -29,6 +30,11 @@ export const ScanSelector = ({
2930
onSelectionChange,
3031
}: SelectScanComplianceDataProps) => {
3132
const selectedScan = scans.find((item) => item.id === selectedScanId);
33+
const triggerLabel =
34+
selectedScan?.attributes.name ||
35+
selectedScan?.providerInfo.alias ||
36+
selectedScan?.providerInfo.uid ||
37+
"";
3238

3339
return (
3440
<Select
@@ -42,7 +48,14 @@ export const ScanSelector = ({
4248
<SelectTrigger className="w-full">
4349
<SelectValue placeholder="Select a scan">
4450
{selectedScan ? (
45-
<ComplianceScanInfo scan={selectedScan} />
51+
<>
52+
<span className="text-text-neutral-secondary shrink-0 text-xs">
53+
Scan:
54+
</span>
55+
<Badge variant="tag" className="truncate">
56+
{triggerLabel}
57+
</Badge>
58+
</>
4659
) : (
4760
"Select a scan"
4861
)}
@@ -53,7 +66,7 @@ export const ScanSelector = ({
5366
<SelectItem
5467
key={scan.id}
5568
value={scan.id}
56-
className="data-[state=checked]:bg-bg-neutral-tertiary"
69+
className="data-[state=checked]:bg-bg-neutral-tertiary [&_svg:not([class*='size-'])]:size-6"
5770
>
5871
<ComplianceScanInfo scan={scan} />
5972
</SelectItem>

ui/components/compliance/compliance-overview-grid.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ export const ComplianceOverviewGrid = ({
4141
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
4242
{filteredFrameworks.map((compliance) => {
4343
const { attributes, id } = compliance;
44-
const { framework, version, requirements_passed, total_requirements } =
45-
attributes;
44+
const {
45+
framework,
46+
version,
47+
requirements_passed,
48+
total_requirements,
49+
} = attributes;
4650

4751
return (
4852
<ComplianceCard

ui/components/compliance/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ export * from "./compliance-custom-details/cis-details";
1313
export * from "./compliance-custom-details/ens-details";
1414
export * from "./compliance-custom-details/iso-details";
1515
export * from "./compliance-download-container";
16-
export * from "./compliance-overview-grid";
16+
export * from "./compliance-header/compliance-filters";
1717
export * from "./compliance-header/compliance-header";
1818
export * from "./compliance-header/compliance-scan-info";
1919
export * from "./compliance-header/data-compliance";
2020
export * from "./compliance-header/scan-selector";
21+
export * from "./compliance-overview-grid";
2122
export * from "./no-scans-available";
2223
export * from "./skeletons/bar-chart-skeleton";
2324
export * from "./skeletons/compliance-accordion-skeleton";

ui/components/compliance/threatscore-badge.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,7 @@ export const ThreatScoreBadge = ({
127127
{Object.entries(sectionScores)
128128
.sort(([, a], [, b]) => a - b)
129129
.map(([section, sectionScore]) => (
130-
<div
131-
key={section}
132-
className="flex items-center gap-2 text-xs"
133-
>
130+
<div key={section} className="flex items-center gap-2 text-xs">
134131
<span className="text-text-neutral-secondary w-1/3 min-w-0 shrink-0 truncate lg:w-1/4">
135132
{section}
136133
</span>

ui/components/findings/table/resource-detail-drawer/use-resource-detail-drawer.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,7 @@ describe("useResourceDetailDrawer — other findings filtering", () => {
274274
const resources = [makeResource()];
275275

276276
getLatestFindingsByResourceUidMock.mockResolvedValue({ data: [] });
277-
adaptFindingsByResourceResponseMock.mockReturnValue([
278-
makeDrawerFinding(),
279-
]);
277+
adaptFindingsByResourceResponseMock.mockReturnValue([makeDrawerFinding()]);
280278

281279
const { result } = renderHook(() =>
282280
useResourceDetailDrawer({

0 commit comments

Comments
 (0)