mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat(scans): add download button column for completed scans in table (#7353)
This commit is contained in:
@@ -760,7 +760,7 @@ export const AddIcon: React.FC<IconSvgProps> = ({
|
||||
};
|
||||
|
||||
export const ScheduleIcon: React.FC<IconSvgProps> = ({
|
||||
size,
|
||||
size = 24,
|
||||
height,
|
||||
width,
|
||||
...props
|
||||
@@ -768,8 +768,9 @@ export const ScheduleIcon: React.FC<IconSvgProps> = ({
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width || 24}
|
||||
height={size || height || 24}
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24" // <- AÑADÍ ESTO
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
@@ -809,6 +810,30 @@ export const InfoIcon: React.FC<IconSvgProps> = ({
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const ManualIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polygon points="10 8 16 12 10 16 10 8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SpinnerIcon: React.FC<IconSvgProps> = ({
|
||||
size = 24,
|
||||
width,
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { toast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
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";
|
||||
import { TriggerIcon } from "../../trigger-icon";
|
||||
import { DataTableRowActions } from "./data-table-row-actions";
|
||||
import { DataTableRowDetails } from "./data-table-row-details";
|
||||
|
||||
@@ -104,6 +110,43 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
return <LinkToFindingsFromScan scanId={id} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "download",
|
||||
header: () => (
|
||||
<div className="flex items-end gap-x-1">
|
||||
<p className="w-fit text-xs">Download</p>
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content="Download a ZIP file containing the JSON (OCSF), CSV, and HTML reports."
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<InfoIcon className="mb-1 text-primary" size={12} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const scanId = row.original.id;
|
||||
const scanState = row.original.attributes?.state;
|
||||
|
||||
return (
|
||||
<div className="flex w-14 items-center justify-center">
|
||||
<CustomButton
|
||||
variant="ghost"
|
||||
isDisabled={scanState !== "completed"}
|
||||
onPress={() => downloadScanZip(scanId, toast)}
|
||||
className="p-0 text-default-500 hover:text-primary disabled:opacity-30"
|
||||
isIconOnly
|
||||
ariaLabel="Download .zip"
|
||||
size="sm"
|
||||
>
|
||||
<DownloadIcon size={16} />
|
||||
</CustomButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// accessorKey: "scanner_args",
|
||||
// header: "Scanner Args",
|
||||
@@ -121,7 +164,11 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
const {
|
||||
attributes: { unique_resource_count },
|
||||
} = getScanData(row);
|
||||
return <p className="font-medium">{unique_resource_count}</p>;
|
||||
return (
|
||||
<div className="flex w-fit items-center justify-center">
|
||||
<span className="text-xs font-medium">{unique_resource_count}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -163,7 +210,11 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
const {
|
||||
attributes: { trigger },
|
||||
} = getScanData(row);
|
||||
return <p className="text-tiny font-medium uppercase">{trigger}</p>;
|
||||
return (
|
||||
<div className="flex w-9 items-center justify-center">
|
||||
<TriggerIcon trigger={trigger} iconSize={16} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -179,8 +230,13 @@ export const ColumnGetScans: ColumnDef<ScanProps>[] = [
|
||||
if (!name || name.length === 0) {
|
||||
return <span className="font-medium">-</span>;
|
||||
}
|
||||
|
||||
return <span className="text-xs font-medium">{name}</span>;
|
||||
return (
|
||||
<div className="flex w-fit items-center justify-center">
|
||||
<span className="text-xs font-medium">
|
||||
{name === "Daily scheduled scan" ? "scheduled scan" : name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,10 +16,10 @@ import { Row } from "@tanstack/react-table";
|
||||
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 { downloadScanZip } from "@/lib/helper";
|
||||
|
||||
import { EditScanForm } from "../../forms";
|
||||
|
||||
@@ -38,41 +38,6 @@ export function DataTableRowActions<ScanProps>({
|
||||
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
|
||||
@@ -103,13 +68,13 @@ export function DataTableRowActions<ScanProps>({
|
||||
color="default"
|
||||
variant="flat"
|
||||
>
|
||||
<DropdownSection title="Export artifacts">
|
||||
<DropdownSection title="Download reports">
|
||||
<DropdownItem
|
||||
key="export"
|
||||
description="Available only for completed scans"
|
||||
textValue="Export Scan Artifacts"
|
||||
textValue="Download .zip"
|
||||
startContent={<DownloadIcon className={iconClasses} />}
|
||||
onPress={handleExportZip}
|
||||
onPress={() => downloadScanZip(scanId, toast)}
|
||||
isDisabled={scanState !== "completed"}
|
||||
>
|
||||
Download .zip
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
|
||||
import { ManualIcon, ScheduleIcon } from "@/components/icons";
|
||||
|
||||
interface TriggerIconProps {
|
||||
trigger: "scheduled" | "manual";
|
||||
iconSize?: number;
|
||||
}
|
||||
|
||||
export function TriggerIcon({ trigger, iconSize = 24 }: TriggerIconProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
className="text-xs"
|
||||
content={trigger === "scheduled" ? "Scheduled" : "Manual"}
|
||||
>
|
||||
<div className="h-fit">
|
||||
{trigger === "scheduled" ? (
|
||||
<ScheduleIcon size={iconSize} />
|
||||
) : (
|
||||
<ManualIcon size={iconSize} />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export const DateWithTime: React.FC<DateWithTimeProps> = ({
|
||||
>
|
||||
<span className="text-xs font-semibold">{formattedDate}</span>
|
||||
{showTime && (
|
||||
<span className="text-tiny text-gray-500">{formattedTime}</span>
|
||||
<span className="text-xs text-gray-500">{formattedTime}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { getExportsZip } from "@/actions/scans";
|
||||
import { getTask } from "@/actions/task";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { AuthSocialProvider, MetaDataProps, PermissionInfo } from "@/types";
|
||||
|
||||
export const baseUrl = process.env.AUTH_URL || "http://localhost:3000";
|
||||
@@ -37,6 +39,42 @@ export const getAuthUrl = (provider: AuthSocialProvider) => {
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const downloadScanZip = async (
|
||||
scanId: string,
|
||||
toast: ReturnType<typeof useToast>["toast"],
|
||||
) => {
|
||||
const result = await getExportsZip(scanId);
|
||||
|
||||
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: "application/zip" });
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const isGoogleOAuthEnabled =
|
||||
!!process.env.SOCIAL_GOOGLE_OAUTH_CLIENT_ID &&
|
||||
!!process.env.SOCIAL_GOOGLE_OAUTH_CLIENT_SECRET;
|
||||
|
||||
Reference in New Issue
Block a user