mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
feat(ui): add CSV and PDF download buttons to compliance views (#10093)
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
92
ui/components/compliance/compliance-download-container.tsx
Normal file
92
ui/components/compliance/compliance-download-container.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user