From faa8a15182332d974b245c9db9ef5c86007d66b2 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 12 Feb 2025 17:31:13 +0100 Subject: [PATCH] feat: allow users to download exports --- ui/actions/scans/scans.ts | 37 +++++++++++++++ .../table/scans/data-table-row-actions.tsx | 46 +++++++++++++++++-- ui/components/ui/toast/Toast.tsx | 4 +- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/ui/actions/scans/scans.ts b/ui/actions/scans/scans.ts index 3ac7d897d6..f40dac98c0 100644 --- a/ui/actions/scans/scans.ts +++ b/ui/actions/scans/scans.ts @@ -198,3 +198,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 25ce5c1ea6..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,14 +13,15 @@ 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"; -import { DownloadIcon } from "lucide-react"; interface DataTableRowActionsProps { row: Row; @@ -31,10 +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 ( <> ({ description="Available only for completed scans" textValue="Export Scan Artifacts" startContent={} - onPress={() => setIsEditOpen(true)} + onPress={handleExportZip} isDisabled={scanState !== "completed"} > - Export .zip + Download .zip diff --git a/ui/components/ui/toast/Toast.tsx b/ui/components/ui/toast/Toast.tsx index ed6797b45c..01940eb9bc 100644 --- a/ui/components/ui/toast/Toast.tsx +++ b/ui/components/ui/toast/Toast.tsx @@ -95,7 +95,7 @@ const ToastTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )); @@ -107,7 +107,7 @@ const ToastDescription = React.forwardRef< >(({ className, ...props }, ref) => ( ));