From f29e87f45bb18fa318c4804a5a2edb514d180c67 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 11 Oct 2024 15:33:03 +0200 Subject: [PATCH] feat: Scan on demand can be executed now from the UI --- actions/providers/providers.ts | 17 +-- actions/scans/scans.ts | 50 ++++++- .../table/data-table-row-actions.tsx | 3 - components/scans/forms/index.ts | 2 + .../scans/forms/scan-on-demand-form.tsx | 126 ++++++++++++++++++ components/scans/forms/schedule-form.tsx | 112 ++++++++++++++++ .../scans/table/data-table-row-actions.tsx | 3 - .../provider-scans/data-table-row-actions.tsx | 108 +++++++-------- components/ui/entities/snippet-label.tsx | 30 +++-- lib/custom.ts | 15 +++ types/formSchemas.ts | 17 +++ 11 files changed, 383 insertions(+), 100 deletions(-) create mode 100644 components/scans/forms/index.ts create mode 100644 components/scans/forms/scan-on-demand-form.tsx create mode 100644 components/scans/forms/schedule-form.tsx diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index 200bc79bd6..4578786534 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { auth } from "@/auth.config"; -import { parseStringify } from "@/lib"; +import { getErrorMessage, parseStringify } from "@/lib"; export const getProviders = async ({ page = 1, @@ -198,18 +198,3 @@ export const deleteProvider = async (formData: FormData) => { }; } }; - -export const getErrorMessage = async (error: unknown): Promise => { - let message: string; - - if (error instanceof Error) { - message = error.message; - } else if (error && typeof error === "object" && "message" in error) { - message = String(error.message); - } else if (typeof error === "string") { - message = error; - } else { - message = "Oops! Something went wrong."; - } - return message; -}; diff --git a/actions/scans/scans.ts b/actions/scans/scans.ts index 81669dba15..06e274b2c2 100644 --- a/actions/scans/scans.ts +++ b/actions/scans/scans.ts @@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { auth } from "@/auth.config"; -import { parseStringify } from "@/lib"; +import { getErrorMessage, parseStringify } from "@/lib"; export const getScans = async ({ page = 1, @@ -46,3 +46,51 @@ export const getScans = async ({ return undefined; } }; + +export const scanOnDemand = async (formData: FormData) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + + const providerId = formData.get("providerId"); + const scanName = formData.get("scanName"); + + const url = new URL(`${keyServer}/scans`); + + try { + const response = await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + body: JSON.stringify({ + data: { + type: "Scan", + attributes: { + name: scanName, + scanner_args: { + checks_to_execute: ["accessanalyzer_enabled"], + }, + }, + relationships: { + provider: { + data: { + type: "Provider", + id: providerId, + }, + }, + }, + }, + }), + }); + const data = await response.json(); + revalidatePath("/scans"); + return parseStringify(data); + } catch (error) { + console.error(error); + return { + error: getErrorMessage(error), + }; + } +}; diff --git a/components/providers/table/data-table-row-actions.tsx b/components/providers/table/data-table-row-actions.tsx index 661b2e86c8..35f14d395c 100644 --- a/components/providers/table/data-table-row-actions.tsx +++ b/components/providers/table/data-table-row-actions.tsx @@ -77,7 +77,6 @@ export function DataTableRowActions({ } > @@ -86,7 +85,6 @@ export function DataTableRowActions({ } onClick={() => setIsEditOpen(true)} @@ -101,7 +99,6 @@ export function DataTableRowActions({ color="danger" description="Delete the provider permanently" textValue="Delete Provider" - shortcut="⌘⇧D" startContent={ >; +}) => { + const formSchema = onDemandScanFormSchema(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + providerId: providerId, + scanName: scanName, + scannerArgs: scannerArgs, + }, + }); + + const { toast } = useToast(); + + const isLoading = form.formState.isSubmitting; + + const onSubmitClient = async (values: z.infer) => { + const formData = new FormData(); + + // Loop through form values and add to formData, converting objects to JSON strings + Object.entries(values).forEach( + ([key, value]) => + value !== undefined && + formData.append( + key, + typeof value === "object" ? JSON.stringify(value) : value, + ), + ); + + const data = await scanOnDemand(formData); + + if (data?.errors && data.errors.length > 0) { + const error = data.errors[0]; + const errorMessage = `${error.detail}`; + // show error + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } else { + toast({ + title: "Success!", + description: "The scan was launched successfully.", + }); + setIsOpen(false); + } + }; + + return ( +
+ + + +
+ +
+ +
+ setIsOpen(false)} + disabled={isLoading} + > + Cancel + + + } + > + {isLoading ? <>Loading : Start now} + +
+
+ + ); +}; diff --git a/components/scans/forms/schedule-form.tsx b/components/scans/forms/schedule-form.tsx new file mode 100644 index 0000000000..d990380891 --- /dev/null +++ b/components/scans/forms/schedule-form.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Dispatch, SetStateAction } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { updateProvider } from "@/actions/providers"; +import { SaveIcon } from "@/components/icons"; +import { useToast } from "@/components/ui"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; +import { scheduleScanFormSchema } from "@/types"; + +export const ScheduleForm = ({ + providerId, + scheduleDate, + setIsOpen, +}: { + providerId: string; + scheduleDate: string; + setIsOpen: Dispatch>; +}) => { + const formSchema = scheduleScanFormSchema(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + providerId: providerId, + scheduleDate: scheduleDate, + }, + }); + + const { toast } = useToast(); + + const isLoading = form.formState.isSubmitting; + + const onSubmitClient = async (values: z.infer) => { + const formData = new FormData(); + + Object.entries(values).forEach( + ([key, value]) => value !== undefined && formData.append(key, value), + ); + const data = await updateProvider(formData); + + if (data?.errors && data.errors.length > 0) { + const error = data.errors[0]; + const errorMessage = `${error.detail}`; + // show error + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } else { + toast({ + title: "Success!", + description: "The scan was scheduled successfully.", + }); + setIsOpen(false); // Close the modal on success + } + }; + + return ( +
+ + + + +
+ setIsOpen(false)} + disabled={isLoading} + > + Cancel + + + } + > + {isLoading ? <>Loading : Schedule} + +
+ + + ); +}; diff --git a/components/scans/table/data-table-row-actions.tsx b/components/scans/table/data-table-row-actions.tsx index fdb4f2d3b0..b37863baf7 100644 --- a/components/scans/table/data-table-row-actions.tsx +++ b/components/scans/table/data-table-row-actions.tsx @@ -78,7 +78,6 @@ export function DataTableRowActions({ } > @@ -88,7 +87,6 @@ export function DataTableRowActions({ } onClick={() => setIsEditOpen(true)} @@ -103,7 +101,6 @@ export function DataTableRowActions({ color="danger" description="Delete the provider permanently" textValue="Delete Provider" - shortcut="⌘⇧D" startContent={ { row: Row; @@ -32,34 +25,41 @@ const iconClasses = export function DataTableRowActions({ row, }: DataTableRowActionsProps) { - const [isEditOpen, setIsEditOpen] = useState(false); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [isScanOnDemandOpen, setIsScanOnDemandOpen] = useState(false); + const [isScanScheduleOpen, setIsScanScheduleOpen] = useState(false); + const providerId = (row.original as { id: string }).id; + const scanName = (row.original as any).attributes?.name; return ( <> - {/* - - */} - {/* + + - - */} + +
- + ({ > Start - {/* */} - + } + key="scanNow" + color="primary" + description="Allows you to start a scan on demand" + textValue="Start now" + startContent={} + onClick={() => setIsScanOnDemandOpen(true)} > - {/* */} - - } - onClick={() => setIsEditOpen(true)} - > - Edit Provider + Start now - + - } - onClick={() => setIsDeleteOpen(true)} + key="schedule" + color="primary" + description="Schedule a scan for this provider" + textValue="Schedule Scan" + startContent={} + onClick={() => setIsScanScheduleOpen(true)} > - Delete Provider + Schedule Scan diff --git a/components/ui/entities/snippet-label.tsx b/components/ui/entities/snippet-label.tsx index 8875976616..18525d6fe5 100644 --- a/components/ui/entities/snippet-label.tsx +++ b/components/ui/entities/snippet-label.tsx @@ -12,19 +12,21 @@ export const SnippetLabel: React.FC = ({ ...props }) => { return ( - } - checkIcon={} - {...props} - > -

- {label} -

-
+ label !== "" && ( + } + checkIcon={} + {...props} + > +

+ {label} +

+
+ ) ); }; diff --git a/lib/custom.ts b/lib/custom.ts index 075f6b95c8..a33f07d6fe 100644 --- a/lib/custom.ts +++ b/lib/custom.ts @@ -19,3 +19,18 @@ export function encryptKey(passkey: string) { export function decryptKey(passkey: string) { return atob(passkey); } + +export const getErrorMessage = async (error: unknown): Promise => { + let message: string; + + if (error instanceof Error) { + message = error.message; + } else if (error && typeof error === "object" && "message" in error) { + message = String(error.message); + } else if (typeof error === "string") { + message = error; + } else { + message = "Oops! Something went wrong."; + } + return message; +}; diff --git a/types/formSchemas.ts b/types/formSchemas.ts index 3726245f68..c03afe2d95 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -1,5 +1,22 @@ import { z } from "zod"; +export const onDemandScanFormSchema = () => + z.object({ + providerId: z.string(), + scanName: z.string().optional(), + scannerArgs: z + .object({ + checksToExecute: z.array(z.string()), + }) + .optional(), + }); + +export const scheduleScanFormSchema = () => + z.object({ + providerId: z.string(), + scheduleDate: z.string(), + }); + export const addProviderFormSchema = z.object({ providerType: z.string(), providerAlias: z.string(),