feat: disable compliance download when region filter is applied

This commit is contained in:
Pablo Lara
2025-05-06 13:42:53 +02:00
parent d33703b437
commit 95f5d6045e
7 changed files with 129 additions and 18 deletions
+39
View File
@@ -257,3 +257,42 @@ export const getExportsZip = async (scanId: string) => {
};
}
};
export const getComplianceCsv = async (
scanId: string,
complianceId: string,
) => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(
`${apiBaseUrl}/scans/${scanId}/compliance/${complianceId}`,
);
try {
const response = await fetch(url.toString(), {
headers,
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData?.errors?.[0]?.detail || "Failed to fetch compliance report",
);
}
// Get the blob data as an array buffer
const arrayBuffer = await response.arrayBuffer();
// Convert to base64
const base64 = Buffer.from(arrayBuffer).toString("base64");
return {
success: true,
data: base64,
filename: `scan-${scanId}-compliance-${complianceId}.csv`,
};
} catch (error) {
return {
error: getErrorMessage(error),
};
}
};
+3
View File
@@ -159,6 +159,7 @@ const SSRComplianceGrid = async ({
framework,
version,
requirements_status: { passed, total },
compliance_id,
} = attributes;
return (
@@ -170,6 +171,8 @@ const SSRComplianceGrid = async ({
totalRequirements={total}
prevPassingRequirements={passed}
prevTotalRequirements={total}
scanId={scanId}
complianceId={compliance_id}
/>
);
})}
+25 -1
View File
@@ -1,7 +1,13 @@
"use client";
import { Card, CardBody, Progress } from "@nextui-org/react";
import Image from "next/image";
import { useSearchParams } from "next/navigation";
import React from "react";
import { DownloadIconButton, toast } from "@/components/ui";
import { downloadComplianceCsv } from "@/lib/helper";
import { getComplianceIcon } from "../icons";
interface ComplianceCardProps {
@@ -11,6 +17,8 @@ interface ComplianceCardProps {
totalRequirements: number;
prevPassingRequirements: number;
prevTotalRequirements: number;
scanId: string;
complianceId: string;
}
export const ComplianceCard: React.FC<ComplianceCardProps> = ({
@@ -18,7 +26,12 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
version,
passingRequirements,
totalRequirements,
scanId,
complianceId,
}) => {
const searchParams = useSearchParams();
const hasRegionFilter = searchParams.has("filter[region__in]");
const formatTitle = (title: string) => {
return title.split("-").join(" ");
};
@@ -27,6 +40,8 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
(passingRequirements / totalRequirements) * 100,
);
// Calculates the percentage change in passing requirements compared to the previous scan.
//
// const prevRatingPercentage = Math.floor(
// (prevPassingRequirements / prevTotalRequirements) * 100,
// );
@@ -79,13 +94,22 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
}}
color={getRatingColor(ratingPercentage)}
/>
<div className="mt-2 flex justify-between">
<div className="mt-2 flex items-center justify-between">
<small>
<span className="mr-1 text-xs font-semibold">
{passingRequirements} / {totalRequirements}
</span>
Passing Requirements
</small>
<DownloadIconButton
paramId={complianceId}
onDownload={() =>
downloadComplianceCsv(scanId, complianceId, toast)
}
textTooltip="Download compliance CSV report"
isDisabled={hasRegionFilter}
/>
{/* <small>{getScanChange()}</small> */}
</div>
</div>
@@ -9,6 +9,7 @@ import { DownloadIconButton, toast } from "@/components/ui";
import { DateWithTime, EntityInfoShort } from "@/components/ui/entities";
import { TriggerSheet } from "@/components/ui/sheet";
import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table";
import { downloadScanZip } from "@/lib/helper";
import { ScanProps } from "@/types";
import { LinkToFindingsFromScan } from "../../link-to-findings-from-scan";
@@ -135,6 +136,7 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
return (
<DownloadIconButton
paramId={scanId}
onDownload={() => downloadScanZip(scanId, toast)}
isDisabled={scanState !== "completed"}
/>
);
@@ -1,34 +1,40 @@
"use client";
import { downloadScanZip } from "@/lib/helper";
import { CustomButton } from "../custom/custom-button";
import { toast } from "@/components/ui";
import { Tooltip } from "@nextui-org/react";
import { DownloadIcon } from "lucide-react";
import { CustomButton } from "../custom/custom-button";
interface DownloadIconButtonProps {
paramId: string;
onDownload: (paramId: string) => void;
ariaLabel?: string;
isDisabled?: boolean;
textTooltip?: string;
}
export const DownloadIconButton = ({
paramId,
onDownload,
ariaLabel = "Download report",
isDisabled = false,
textTooltip = "Download report",
}: DownloadIconButtonProps) => {
return (
<div className="flex w-14 items-center justify-center">
<CustomButton
variant="ghost"
isDisabled={isDisabled}
onPress={() => downloadScanZip(paramId, toast)}
className="p-0 text-default-500 hover:text-primary disabled:opacity-30"
isIconOnly
ariaLabel={ariaLabel}
size="sm"
>
<DownloadIcon size={16} />
</CustomButton>
<div className="flex items-center justify-end">
<Tooltip content={textTooltip} className="text-xs">
<CustomButton
variant="ghost"
isDisabled={isDisabled}
onPress={() => onDownload(paramId)}
className="p-0 text-default-500 hover:text-primary disabled:opacity-30"
isIconOnly
ariaLabel={ariaLabel}
size="sm"
>
<DownloadIcon size={16} />
</CustomButton>
</Tooltip>
</div>
);
};
+1 -1
View File
@@ -4,6 +4,7 @@ export * from "./alert-dialog/AlertDialog";
export * from "./chart/Chart";
export * from "./content-layout/content-layout";
export * from "./dialog/dialog";
export * from "./download-icon-button/download-icon-button";
export * from "./dropdown/Dropdown";
export * from "./headers/navigation-header";
export * from "./label/Label";
@@ -11,4 +12,3 @@ export * from "./main-layout/main-layout";
export * from "./select/Select";
export * from "./sidebar";
export * from "./toast";
export * from "./download-icon-button/download-icon-button";
+38 -1
View File
@@ -1,4 +1,4 @@
import { getExportsZip } from "@/actions/scans";
import { getComplianceCsv, getExportsZip } from "@/actions/scans";
import { getTask } from "@/actions/task";
import { auth } from "@/auth.config";
import { useToast } from "@/components/ui";
@@ -91,6 +91,43 @@ export const downloadScanZip = async (
}
};
export const downloadComplianceCsv = async (
scanId: string,
complianceId: string,
toast: ReturnType<typeof useToast>["toast"],
) => {
const result = await getComplianceCsv(scanId, complianceId);
if (result?.success && result?.data) {
const binaryString = window.atob(result.data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const blob = new Blob([bytes], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = result.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
toast({
title: "Download Complete",
description: "The compliance report has been downloaded successfully.",
});
} else if (result?.error) {
toast({
variant: "destructive",
title: "Download Failed",
description: result.error,
});
}
};
export const isGoogleOAuthEnabled =
!!process.env.SOCIAL_GOOGLE_OAUTH_CLIENT_ID &&
!!process.env.SOCIAL_GOOGLE_OAUTH_CLIENT_SECRET;