Skip to content

Commit

Permalink
feat(exports): download scan exports (#7006)
Browse files Browse the repository at this point in the history
  • Loading branch information
paabloLC authored Feb 27, 2025
1 parent 81c7ebf commit 1180522
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 3 deletions.
37 changes: 37 additions & 0 deletions ui/actions/scans/scans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};
}
};
54 changes: 53 additions & 1 deletion ui/components/scans/table/scans/data-table-row-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -30,9 +32,47 @@ const iconClasses =
export function DataTableRowActions<ScanProps>({
row,
}: DataTableRowActionsProps<ScanProps>) {
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 (
<>
<CustomAlertModal
Expand Down Expand Up @@ -63,6 +103,18 @@ export function DataTableRowActions<ScanProps>({
color="default"
variant="flat"
>
<DropdownSection title="Export artifacts">
<DropdownItem
key="export"
description="Available only for completed scans"
textValue="Export Scan Artifacts"
startContent={<DownloadIcon className={iconClasses} />}
onPress={handleExportZip}
isDisabled={scanState !== "completed"}
>
Download .zip
</DropdownItem>
</DropdownSection>
<DropdownSection title="Actions">
<DropdownItem
key="edit"
Expand Down
4 changes: 2 additions & 2 deletions ui/components/ui/toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const ToastTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("[&+div]:text-md text-lg font-semibold", className)}
className={cn("[&+div]:text-md font-semibold", className)}
{...props}
/>
));
Expand All @@ -107,7 +107,7 @@ const ToastDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-md opacity-90", className)}
className={cn("text-small opacity-90", className)}
{...props}
/>
));
Expand Down

0 comments on commit 1180522

Please sign in to comment.