Files
prowler/ui/components/scans/table/scan-jobs-row-actions.tsx
T

266 lines
7.7 KiB
TypeScript

"use client";
import {
CalendarClock,
Download,
Eye,
Pencil,
ShieldCheck,
TriangleAlert,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { getSchedule } from "@/actions/schedules";
import { getTask } from "@/actions/task";
import { getScanErrorDetails } from "@/actions/task/task.adapter";
import { EditAliasModal } from "@/components/scans/edit-alias-modal";
import {
ScanErrorDetailsModal,
type ScanErrorDetailsState,
} from "@/components/scans/scan-error-details-modal";
import {
EDIT_SCAN_SCHEDULE_STATE,
EditScanScheduleModal,
type EditScanScheduleState,
} from "@/components/scans/schedule/edit-scan-schedule-modal";
import { useToast } from "@/components/shadcn";
import {
ActionDropdown,
ActionDropdownItem,
} from "@/components/shadcn/dropdown";
import { useReportDownload } from "@/hooks/use-report-download";
import { buildPerScanComplianceHref } from "@/lib/compliance/compliance-tab-url";
import { downloadScanZip } from "@/lib/helper";
import { getScanScheduleCapability } from "@/lib/schedules";
import { isCloud } from "@/lib/shared/env";
import {
type ProviderType,
SCAN_JOBS_TAB,
type ScanJobsTab,
type ScanProps,
type ScheduleApiResponse,
} from "@/types";
import {
SCAN_SCHEDULE_CAPABILITY,
type ScanScheduleCapability,
type ScanScheduleProvider,
} from "@/types/schedules";
interface ScanJobsRowActionsProps {
scan: ScanProps;
/** The scan jobs tab this row is rendered in. */
tab: ScanJobsTab;
/**
* Schedule capability override. Only for Prowler Cloud.
*/
capability?: ScanScheduleCapability;
/** Prowler Cloud tenants without a paid plan cannot download reports. */
subscriptionOnly?: boolean;
}
export function ScanJobsRowActions({
scan,
tab,
capability,
subscriptionOnly = false,
}: ScanJobsRowActionsProps) {
const router = useRouter();
const runReportDownload = useReportDownload(subscriptionOnly);
const canEditSchedule =
(capability ?? getScanScheduleCapability(isCloud())) ===
SCAN_SCHEDULE_CAPABILITY.ADVANCED;
const { toast } = useToast();
const [editOpen, setEditOpen] = useState(false);
const [scheduleOpen, setScheduleOpen] = useState(false);
const [scheduleState, setScheduleState] = useState<EditScanScheduleState>({
kind: EDIT_SCAN_SCHEDULE_STATE.LOADING,
});
const [errorOpen, setErrorOpen] = useState(false);
const [errorState, setErrorState] = useState<ScanErrorDetailsState>({
kind: "idle",
});
const scanState = scan.attributes.state;
const isCompleted = scanState === "completed";
const isFailed = scanState === "failed";
// Prowler Cloud partial scans re-check a few resources: they compute no
// compliance and write no report files, so neither entry applies.
const isPartial = scan.attributes.is_partial === true;
const taskId = scan.relationships.task.data?.id;
// The findings page bounds the UTC day range with completed_at; without it the
// range collapses to the start day and can miss later findings.
const hasCompletedAt = Boolean(scan.attributes.completed_at);
const providerId = scan.relationships.provider.data?.id;
const scheduleProvider: ScanScheduleProvider | undefined = providerId
? {
providerId,
providerType: (scan.providerInfo?.provider ?? "aws") as ProviderType,
providerUid: scan.providerInfo?.uid ?? providerId,
providerAlias: scan.providerInfo?.alias ?? null,
}
: undefined;
const openFindings = () => {
if (!isCompleted || !hasCompletedAt) return;
router.push(
`/findings?filter[scan__in]=${scan.id}&filter[status__in]=FAIL`,
);
};
const openCompliance = () => {
if (!isCompleted) return;
router.push(buildPerScanComplianceHref({ scanId: scan.id }));
};
const openErrorDetails = async () => {
setErrorOpen(true);
setErrorState({ kind: "loading" });
if (!taskId) {
setErrorState({
kind: "error",
message: "Task ID is not available for this scan.",
});
return;
}
const response: unknown = await getTask(taskId);
if (
typeof response === "object" &&
response !== null &&
"error" in response &&
typeof (response as { error: unknown }).error === "string"
) {
setErrorState({
kind: "error",
message: (response as { error: string }).error,
});
return;
}
const details = getScanErrorDetails(response);
if (!details) {
setErrorState({
kind: "error",
message: "No error details were found for this failed scan.",
});
return;
}
setErrorState({ kind: "loaded", details });
};
const openScheduleEditor = async () => {
if (!providerId) {
setScheduleState({
kind: EDIT_SCAN_SCHEDULE_STATE.ERROR,
message: "Provider ID is not available for this scan.",
});
setScheduleOpen(true);
return;
}
setScheduleState({ kind: EDIT_SCAN_SCHEDULE_STATE.LOADING });
setScheduleOpen(true);
const response = (await getSchedule(providerId)) as
| ScheduleApiResponse
| { error?: string };
if (!response || ("error" in response && response.error)) {
setScheduleState({
kind: EDIT_SCAN_SCHEDULE_STATE.ERROR,
message:
response && "error" in response && response.error
? response.error
: "Failed to load scan schedule.",
});
return;
}
setScheduleState({
kind: EDIT_SCAN_SCHEDULE_STATE.LOADED,
schedule: "data" in response ? response.data : null,
});
};
return (
<div className="flex items-center justify-end">
<ActionDropdown>
{/* Schedule editing belongs only to the Scheduled tab. */}
{tab === SCAN_JOBS_TAB.SCHEDULED && canEditSchedule && (
<ActionDropdownItem
icon={<CalendarClock />}
label="Edit Scan Schedule"
onSelect={() => void openScheduleEditor()}
/>
)}
{/* Synthetic pending rows have no Scan to alias. */}
{!scan.pendingSchedule && (
<ActionDropdownItem
icon={<Pencil />}
label="Edit Scan Alias"
onSelect={() => setEditOpen(true)}
/>
)}
{isCompleted && (
<>
<ActionDropdownItem
icon={<Eye />}
label="View Findings"
onSelect={openFindings}
disabled={!isCompleted || !hasCompletedAt}
/>
{!isPartial && (
<ActionDropdownItem
icon={<ShieldCheck />}
label="View Compliance"
onSelect={openCompliance}
/>
)}
{!isPartial && (
<ActionDropdownItem
icon={<Download />}
label="Download Scan Reports"
onSelect={() =>
runReportDownload(() => downloadScanZip(scan.id, toast))
}
/>
)}
</>
)}
{isFailed && (
<ActionDropdownItem
icon={<TriangleAlert />}
label="View error details"
onSelect={() => void openErrorDetails()}
/>
)}
{/* TODO: Restore Cancel Scan once the backend exposes a public scan cancellation endpoint. */}
</ActionDropdown>
<EditAliasModal
open={editOpen}
onOpenChange={setEditOpen}
scanId={scan.id}
currentAlias={scan.attributes.name ?? ""}
/>
<ScanErrorDetailsModal
open={errorOpen}
onOpenChange={setErrorOpen}
state={errorState}
/>
<EditScanScheduleModal
open={scheduleOpen}
onOpenChange={setScheduleOpen}
provider={scheduleProvider}
state={scheduleState}
/>
</div>
);
}