From eeef6600b7af9947222d11d89bbe14080c9a333d Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 27 Feb 2025 09:23:12 +0100 Subject: [PATCH] feat(exports): download scan exports (#7006) --- ui/actions/scans/scans.ts | 37 +++++++++++++ .../table/scans/data-table-row-actions.tsx | 54 ++++++++++++++++++- ui/components/ui/toast/Toast.tsx | 4 +- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index a1064a1085..954b600a26 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -247,3 +247,40 @@ export const updateScan = async (formData: FormData) => { }; } }; + +export const getExportsZip = async (scanId: string) => { + const session = await auth(); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/scans/${scanId}/report`); + + try { + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData?.errors?.[0]?.detail || "Failed to fetch 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}-report.zip`, + }; + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; diff --git a/ui/components/scans/table/scans/data-table-row-actions.tsx b/ui/components/scans/table/scans/data-table-row-actions.tsx index 7a01023986..46c1111bd3 100644 --- a/ui/components/scans/table/scans/data-table-row-actions.tsx +++ b/ui/components/scans/table/scans/data-table-row-actions.tsx @@ -13,10 +13,12 @@ import { EditDocumentBulkIcon, } from "@nextui-org/shared-icons"; import { Row } from "@tanstack/react-table"; -// import clsx from "clsx"; +import { DownloadIcon } from "lucide-react"; import { useState } from "react"; +import { getExportsZip } from "@/actions/scans"; import { VerticalDotsIcon } from "@/components/icons"; +import { useToast } from "@/components/ui"; import { CustomAlertModal } from "@/components/ui/custom"; import { EditScanForm } from "../../forms"; @@ -30,9 +32,47 @@ const iconClasses = export function DataTableRowActions({ row, }: DataTableRowActionsProps) { + const { toast } = useToast(); const [isEditOpen, setIsEditOpen] = useState(false); const scanId = (row.original as { id: string }).id; const scanName = (row.original as any).attributes?.name; + const scanState = (row.original as any).attributes?.state; + + const handleExportZip = async () => { + const result = await getExportsZip(scanId); + + if (result?.success && result?.data) { + // Convert base64 to blob + 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: "application/zip" }); + + // Create download link + 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: "Your scan report has been downloaded successfully.", + }); + } else if (result?.error) { + toast({ + variant: "destructive", + title: "Download Failed", + description: result.error, + }); + } + }; + return ( <> ({ color="default" variant="flat" > + + } + onPress={handleExportZip} + isDisabled={scanState !== "completed"} + > + Download .zip + + (({ className, ...props }, ref) => ( )); @@ -107,7 +107,7 @@ const ToastDescription = React.forwardRef< >(({ className, ...props }, ref) => ( ));