feat(ui): add CSV and PDF download buttons to compliance views (#10093)

This commit is contained in:
Pedro Martín
2026-02-18 09:36:54 +01:00
committed by GitHub
parent 7698cdce2e
commit 313da7ebf5
6 changed files with 115 additions and 136 deletions

View File

@@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🚀 Added
- PDF report available for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088)
- CSV and PDF download buttons in compliance views [(#10093)](https://github.com/prowler-cloud/prowler/pull/10093)
### 🔄 Changed

View File

@@ -9,7 +9,7 @@ import {
import { getThreatScore } from "@/actions/overview";
import {
ClientAccordionWrapper,
ComplianceDownloadButton,
ComplianceDownloadContainer,
ComplianceHeader,
RequirementsStatusCard,
RequirementsStatusCardSkeleton,
@@ -128,20 +128,17 @@ export default async function ComplianceDetail({
selectedScan={selectedScan}
/>
</div>
{(() => {
const framework = attributesData?.data?.[0]?.attributes?.framework;
const reportType = getReportTypeForFramework(framework);
return selectedScanId && reportType ? (
<div className="mb-4 flex-shrink-0 self-end sm:mb-0 sm:self-start sm:pt-1">
<ComplianceDownloadButton
scanId={selectedScanId}
reportType={reportType}
label="Download report"
/>
</div>
) : null;
})()}
{selectedScanId && (
<div className="mb-4 flex-shrink-0 self-end sm:mb-0 sm:self-start sm:pt-1">
<ComplianceDownloadContainer
scanId={selectedScanId}
complianceId={complianceId}
reportType={getReportTypeForFramework(
attributesData?.data?.[0]?.attributes?.framework,
)}
/>
</div>
)}
</div>
<Suspense

View File

@@ -3,14 +3,13 @@
import { Progress } from "@heroui/progress";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
import { Card, CardContent } from "@/components/shadcn/card/card";
import { DownloadIconButton, toast } from "@/components/ui";
import { downloadComplianceCsv } from "@/lib/helper";
import { getReportTypeForFramework } from "@/lib/compliance/compliance-report-types";
import { ScanEntity } from "@/types/scans";
import { getComplianceIcon } from "../icons";
import { ComplianceDownloadContainer } from "./compliance-download-container";
interface ComplianceCardProps {
title: string;
@@ -38,7 +37,6 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
const searchParams = useSearchParams();
const router = useRouter();
const hasRegionFilter = searchParams.has("filter[region__in]");
const [isDownloading, setIsDownloading] = useState<boolean>(false);
const formatTitle = (title: string) => {
return title.split("-").join(" ");
@@ -85,14 +83,6 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
router.push(`${path}?${params.toString()}`);
};
const handleDownload = async () => {
setIsDownloading(true);
try {
await downloadComplianceCsv(scanId, complianceId, toast);
} finally {
setIsDownloading(false);
}
};
return (
<Card
@@ -143,15 +133,15 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
e.stopPropagation();
}
}}
role="button"
role="group"
tabIndex={0}
>
<DownloadIconButton
paramId={complianceId}
onDownload={handleDownload}
textTooltip="Download compliance CSV report"
isDisabled={hasRegionFilter}
isDownloading={isDownloading}
<ComplianceDownloadContainer
compact
scanId={scanId}
complianceId={complianceId}
reportType={getReportTypeForFramework(title)}
disabled={hasRegionFilter}
/>
</div>
</div>

View File

@@ -1,101 +0,0 @@
"use client";
import { DownloadIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/shadcn/button/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/shadcn/tooltip";
import { toast } from "@/components/ui";
import {
COMPLIANCE_REPORT_BUTTON_LABELS,
type ComplianceReportType,
} from "@/lib/compliance/compliance-report-types";
import { downloadComplianceReportPdf } from "@/lib/helper";
interface ComplianceDownloadButtonProps {
scanId: string;
reportType: ComplianceReportType;
label?: string;
/** Show only icon with tooltip on mobile (sm and below) */
iconOnlyOnMobile?: boolean;
}
export const ComplianceDownloadButton = ({
scanId,
reportType,
label,
iconOnlyOnMobile = false,
}: ComplianceDownloadButtonProps) => {
const [isDownloading, setIsDownloading] = useState<boolean>(false);
const handleDownload = async () => {
setIsDownloading(true);
try {
await downloadComplianceReportPdf(scanId, reportType, toast);
} finally {
setIsDownloading(false);
}
};
const defaultLabel = COMPLIANCE_REPORT_BUTTON_LABELS[reportType];
const buttonLabel = label || defaultLabel;
if (iconOnlyOnMobile) {
return (
<>
{/* Mobile and Tablet: Icon only with tooltip */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="icon"
onClick={handleDownload}
disabled={isDownloading}
className="md:hidden"
aria-label={buttonLabel}
>
<DownloadIcon
className={isDownloading ? "animate-download-icon" : ""}
size={16}
/>
</Button>
</TooltipTrigger>
<TooltipContent>{buttonLabel}</TooltipContent>
</Tooltip>
{/* Desktop: Full button with label */}
<Button
variant="default"
size="sm"
onClick={handleDownload}
disabled={isDownloading}
className="hidden md:inline-flex"
>
<DownloadIcon
className={isDownloading ? "animate-download-icon" : ""}
size={16}
/>
{buttonLabel}
</Button>
</>
);
}
return (
<Button
variant="default"
size="sm"
onClick={handleDownload}
disabled={isDownloading}
>
<DownloadIcon
className={isDownloading ? "animate-download-icon" : ""}
size={16}
/>
{buttonLabel}
</Button>
);
};

View File

@@ -0,0 +1,92 @@
"use client";
import { DownloadIcon, FileTextIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/shadcn/button/button";
import { toast } from "@/components/ui";
import type { ComplianceReportType } from "@/lib/compliance/compliance-report-types";
import {
downloadComplianceCsv,
downloadComplianceReportPdf,
} from "@/lib/helper";
import { cn } from "@/lib/utils";
interface ComplianceDownloadContainerProps {
scanId: string;
complianceId: string;
reportType?: ComplianceReportType;
compact?: boolean;
disabled?: boolean;
}
export const ComplianceDownloadContainer = ({
scanId,
complianceId,
reportType,
compact = false,
disabled = false,
}: ComplianceDownloadContainerProps) => {
const [isDownloadingCsv, setIsDownloadingCsv] = useState(false);
const [isDownloadingPdf, setIsDownloadingPdf] = useState(false);
const handleDownloadCsv = async () => {
if (isDownloadingCsv) return;
setIsDownloadingCsv(true);
try {
await downloadComplianceCsv(scanId, complianceId, toast);
} finally {
setIsDownloadingCsv(false);
}
};
const handleDownloadPdf = async () => {
if (!reportType || isDownloadingPdf) return;
setIsDownloadingPdf(true);
try {
await downloadComplianceReportPdf(scanId, reportType, toast);
} finally {
setIsDownloadingPdf(false);
}
};
const buttonClassName = cn(
"border-button-primary text-button-primary hover:bg-button-primary/10",
compact && "h-7 px-2 text-xs",
);
return (
<div className={cn("flex gap-2", compact ? "items-center" : "flex-col")}>
<Button
size="sm"
variant="outline"
className={buttonClassName}
onClick={handleDownloadCsv}
disabled={disabled || isDownloadingCsv}
aria-label="Download compliance CSV report"
>
<FileTextIcon
size={14}
className={isDownloadingCsv ? "animate-download-icon" : ""}
/>
CSV
</Button>
{reportType && (
<Button
size="sm"
variant="outline"
className={buttonClassName}
onClick={handleDownloadPdf}
disabled={disabled || isDownloadingPdf}
aria-label="Download compliance PDF report"
>
<DownloadIcon
size={14}
className={isDownloadingPdf ? "animate-download-icon" : ""}
/>
PDF
</Button>
)}
</div>
);
};

View File

@@ -12,7 +12,7 @@ export * from "./compliance-charts/top-failed-sections-card";
export * from "./compliance-custom-details/cis-details";
export * from "./compliance-custom-details/ens-details";
export * from "./compliance-custom-details/iso-details";
export * from "./compliance-download-button";
export * from "./compliance-download-container";
export * from "./compliance-header/compliance-header";
export * from "./compliance-header/compliance-scan-info";
export * from "./compliance-header/data-compliance";