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 ### 🚀 Added
- PDF report available for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088) - 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 ### 🔄 Changed

View File

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

View File

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