From 260f007e5b44fc9c17adbc3fabb65b569299ffab Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 7 Nov 2024 17:02:23 +0100 Subject: [PATCH 1/8] chore: adding a select component to choose a provider to launch a scan --- app/(prowler)/scans/page.tsx | 12 ++-- components/filters/data-filters.ts | 5 ++ components/scans/launch-workflow/index.ts | 2 + .../launch-workflow/launch-scan-workflow.tsx | 31 ++++++++ .../launch-workflow/select-scan-provider.tsx | 70 +++++++++++++++++++ 5 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 components/scans/launch-workflow/index.ts create mode 100644 components/scans/launch-workflow/launch-scan-workflow.tsx create mode 100644 components/scans/launch-workflow/select-scan-provider.tsx diff --git a/app/(prowler)/scans/page.tsx b/app/(prowler)/scans/page.tsx index be74f9a28a..44e5881609 100644 --- a/app/(prowler)/scans/page.tsx +++ b/app/(prowler)/scans/page.tsx @@ -3,8 +3,9 @@ import { Suspense } from "react"; import { getProviders } from "@/actions/providers"; import { getScans } from "@/actions/scans"; -import { FilterControls, filterScans } from "@/components/filters"; +import { filterScans } from "@/components/filters"; import { InfoIcon } from "@/components/icons"; +import { LaunchScanWorkflow } from "@/components/scans/launch-workflow"; import { SkeletonTableScans } from "@/components/scans/table"; import { ColumnProviderScans } from "@/components/scans/table/provider-scans"; import { ColumnGetScans } from "@/components/scans/table/scans"; @@ -24,7 +25,9 @@ export default async function Scans({
- + }> + +
@@ -44,8 +47,9 @@ export default async function Scans({ } const SSRDataTableProviders = async () => { - const filters = { "filter[connected]": "true" }; - const providersData = await getProviders({ page: 1, filters }); + const providersData = await getProviders({ + filters: { "filter[connected]": "true" }, + }); return ( <>
diff --git a/components/filters/data-filters.ts b/components/filters/data-filters.ts index 2b2d319783..673e4f0fff 100644 --- a/components/filters/data-filters.ts +++ b/components/filters/data-filters.ts @@ -8,6 +8,11 @@ export const filterProviders = [ ]; export const filterScans = [ + { + key: "provider_type__in", + labelCheckboxGroup: "Provider", + values: ["aws", "azure", "gcp", "kubernetes"], + }, { key: "state", labelCheckboxGroup: "State", diff --git a/components/scans/launch-workflow/index.ts b/components/scans/launch-workflow/index.ts new file mode 100644 index 0000000000..4ccdef368b --- /dev/null +++ b/components/scans/launch-workflow/index.ts @@ -0,0 +1,2 @@ +export * from "./launch-scan-workflow"; +export * from "./select-scan-provider"; diff --git a/components/scans/launch-workflow/launch-scan-workflow.tsx b/components/scans/launch-workflow/launch-scan-workflow.tsx new file mode 100644 index 0000000000..4fb3f4f16b --- /dev/null +++ b/components/scans/launch-workflow/launch-scan-workflow.tsx @@ -0,0 +1,31 @@ +import { getProviders } from "@/actions/providers/providers"; +import { ProviderProps } from "@/types"; + +import { SelectScanProvider } from "./select-scan-provider"; + +export const LaunchScanWorkflow = async () => { + // const providersData = await getProviders({ + // filters: { "filter[connected]": "true" }, + // }); + const providersData = await getProviders({}); + + const providerInfo = providersData?.data?.length + ? providersData.data.map((provider: ProviderProps) => ({ + alias: provider.attributes.alias, + providerType: provider.attributes.provider, + uid: provider.attributes.uid, + connected: provider.attributes.connection.connected, + })) + : []; + + return ( +
+
+
+ Launch Scan + +
+
+
+ ); +}; diff --git a/components/scans/launch-workflow/select-scan-provider.tsx b/components/scans/launch-workflow/select-scan-provider.tsx new file mode 100644 index 0000000000..9fcc2e70da --- /dev/null +++ b/components/scans/launch-workflow/select-scan-provider.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { Select, SelectItem } from "@nextui-org/react"; +import { useState } from "react"; + +import { AWSProviderBadge } from "@/components/icons/providers-badge/AWSProviderBadge"; +import { AzureProviderBadge } from "@/components/icons/providers-badge/AzureProviderBadge"; +import { GCPProviderBadge } from "@/components/icons/providers-badge/GCPProviderBadge"; +import { KS8ProviderBadge } from "@/components/icons/providers-badge/KS8ProviderBadge"; + +interface SelectScanProviderProps { + providers: { + alias: string; + providerType: string; + uid: string; + connected: boolean; + }[]; +} + +export const SelectScanProvider = ({ providers }: SelectScanProviderProps) => { + const [selectedKeys, setSelectedKeys] = useState>(new Set()); + + const renderBadge = (providerType: string) => { + switch (providerType) { + case "aws": + return ; + case "azure": + return ; + case "gcp": + return ; + case "kubernetes": + return ; + default: + return null; + } + }; + + return ( + + ); +}; From 1c37b58177fd2f08c5f338e83333afb2fa7c03dc Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 8 Nov 2024 08:11:24 +0100 Subject: [PATCH 2/8] delete: remove old component --- .../ui/custom/CustomButtonClientAction.tsx | 30 ------------------- components/ui/custom/index.ts | 1 - 2 files changed, 31 deletions(-) delete mode 100644 components/ui/custom/CustomButtonClientAction.tsx diff --git a/components/ui/custom/CustomButtonClientAction.tsx b/components/ui/custom/CustomButtonClientAction.tsx deleted file mode 100644 index bafc945e17..0000000000 --- a/components/ui/custom/CustomButtonClientAction.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import { Button, CircularProgress } from "@nextui-org/react"; -import clsx from "clsx"; -import { useFormStatus } from "react-dom"; - -interface CustomButtonClientActionProps { - buttonLabel: string; - danger?: boolean; -} - -export const CustomButtonClientAction = ({ - buttonLabel, - danger = false, -}: CustomButtonClientActionProps) => { - const { pending } = useFormStatus(); - - return ( - - ); -}; diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts index 130646a89e..58a00e4d1d 100644 --- a/components/ui/custom/index.ts +++ b/components/ui/custom/index.ts @@ -5,4 +5,3 @@ export * from "./custom-dropdown-filter"; export * from "./custom-input"; export * from "./custom-loader"; export * from "./custom-radio"; -export * from "./CustomButtonClientAction"; From 58018578837731689e92e39a556eee64a3cc2cfd Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 8 Nov 2024 08:45:27 +0100 Subject: [PATCH 3/8] feat: dynamic provider selector for scan launch --- app/(prowler)/scans/page.tsx | 20 ++- .../launch-workflow/launch-scan-workflow.tsx | 144 +++++++++++++++--- .../launch-workflow/select-scan-provider.tsx | 100 +++++++----- 3 files changed, 203 insertions(+), 61 deletions(-) diff --git a/app/(prowler)/scans/page.tsx b/app/(prowler)/scans/page.tsx index 44e5881609..7f97d572d3 100644 --- a/app/(prowler)/scans/page.tsx +++ b/app/(prowler)/scans/page.tsx @@ -11,7 +11,7 @@ import { ColumnProviderScans } from "@/components/scans/table/provider-scans"; import { ColumnGetScans } from "@/components/scans/table/scans"; import { Header } from "@/components/ui"; import { DataTable } from "@/components/ui/table"; -import { SearchParamsProps } from "@/types"; +import { ProviderProps, SearchParamsProps } from "@/types"; export default async function Scans({ searchParams, @@ -19,15 +19,27 @@ export default async function Scans({ searchParams: SearchParamsProps; }) { const searchParamsKey = JSON.stringify(searchParams || {}); + // const providersData = await getProviders({ + // filters: { "filter[connected]": "true" }, + // }); + const providersData = await getProviders({}); + + const providerInfo = providersData?.data?.length + ? providersData.data.map((provider: ProviderProps) => ({ + providerId: provider.id, + alias: provider.attributes.alias, + providerType: provider.attributes.provider, + uid: provider.attributes.uid, + connected: provider.attributes.connection.connected, + })) + : []; return ( <>
- }> - - +
diff --git a/components/scans/launch-workflow/launch-scan-workflow.tsx b/components/scans/launch-workflow/launch-scan-workflow.tsx index 4fb3f4f16b..00423ef8e5 100644 --- a/components/scans/launch-workflow/launch-scan-workflow.tsx +++ b/components/scans/launch-workflow/launch-scan-workflow.tsx @@ -1,31 +1,131 @@ -import { getProviders } from "@/actions/providers/providers"; -import { ProviderProps } from "@/types"; +"use client"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { scanOnDemand } from "@/actions/scans"; +import { RocketIcon } from "@/components/icons"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; +import { toast } from "@/components/ui/toast"; +import { onDemandScanFormSchema } from "@/types"; import { SelectScanProvider } from "./select-scan-provider"; -export const LaunchScanWorkflow = async () => { - // const providersData = await getProviders({ - // filters: { "filter[connected]": "true" }, - // }); - const providersData = await getProviders({}); +type ProviderInfo = { + providerId: string; + alias: string; + providerType: string; + uid: string; + connected: boolean; +}; - const providerInfo = providersData?.data?.length - ? providersData.data.map((provider: ProviderProps) => ({ - alias: provider.attributes.alias, - providerType: provider.attributes.provider, - uid: provider.attributes.uid, - connected: provider.attributes.connection.connected, - })) - : []; +export const LaunchScanWorkflow = ({ + providers, +}: { + providers: ProviderInfo[]; +}) => { + const formSchema = onDemandScanFormSchema(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + providerId: "", + scanName: "", + scannerArgs: { checksToExecute: [] }, + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmitClient = async (values: z.infer) => { + const formData = new FormData(); + console.log(values); + + // 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.", + }); + } + }; return ( -
-
-
- Launch Scan - +
+ +
+
+
+ Launch Scan + +
+ +
-
-
+
+ + Cancel + + + } + > + {isLoading ? <>Loading : Start now} + +
+ + ); }; diff --git a/components/scans/launch-workflow/select-scan-provider.tsx b/components/scans/launch-workflow/select-scan-provider.tsx index 9fcc2e70da..ff9d1d0917 100644 --- a/components/scans/launch-workflow/select-scan-provider.tsx +++ b/components/scans/launch-workflow/select-scan-provider.tsx @@ -1,25 +1,37 @@ "use client"; import { Select, SelectItem } from "@nextui-org/react"; -import { useState } from "react"; +import { Control, FieldPath, FieldValues } from "react-hook-form"; import { AWSProviderBadge } from "@/components/icons/providers-badge/AWSProviderBadge"; import { AzureProviderBadge } from "@/components/icons/providers-badge/AzureProviderBadge"; import { GCPProviderBadge } from "@/components/icons/providers-badge/GCPProviderBadge"; import { KS8ProviderBadge } from "@/components/icons/providers-badge/KS8ProviderBadge"; +import { FormControl, FormField, FormMessage } from "@/components/ui/form"; -interface SelectScanProviderProps { +interface SelectScanProviderProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> { providers: { + providerId: string; alias: string; providerType: string; uid: string; connected: boolean; }[]; + control: Control; + name: TName; } -export const SelectScanProvider = ({ providers }: SelectScanProviderProps) => { - const [selectedKeys, setSelectedKeys] = useState>(new Set()); - +export const SelectScanProvider = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + providers, + control, + name, +}: SelectScanProviderProps) => { const renderBadge = (providerType: string) => { switch (providerType) { case "aws": @@ -36,35 +48,53 @@ export const SelectScanProvider = ({ providers }: SelectScanProviderProps) => { }; return ( - + ( + <> + + + + + + )} + /> ); }; From df732342348d11c1f6723cad0490af351e925765 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 8 Nov 2024 09:41:48 +0100 Subject: [PATCH 4/8] feat: the new workflow to launch a scan is almost done --- app/(prowler)/scans/page.tsx | 37 +---- components/scans/forms/schedule-form.tsx | 2 +- .../launch-workflow/launch-scan-workflow.tsx | 148 ++++++++++++------ .../launch-workflow/select-scan-provider.tsx | 2 +- .../provider-scans/column-provider-scans.tsx | 39 ----- .../provider-scans/data-table-row-actions.tsx | 110 ------------- .../scans/table/provider-scans/index.ts | 2 - 7 files changed, 107 insertions(+), 233 deletions(-) delete mode 100644 components/scans/table/provider-scans/column-provider-scans.tsx delete mode 100644 components/scans/table/provider-scans/data-table-row-actions.tsx delete mode 100644 components/scans/table/provider-scans/index.ts diff --git a/app/(prowler)/scans/page.tsx b/app/(prowler)/scans/page.tsx index 7f97d572d3..70ded7e723 100644 --- a/app/(prowler)/scans/page.tsx +++ b/app/(prowler)/scans/page.tsx @@ -1,13 +1,11 @@ -import { Link, Spacer, Tooltip } from "@nextui-org/react"; +import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getProviders } from "@/actions/providers"; import { getScans } from "@/actions/scans"; import { filterScans } from "@/components/filters"; -import { InfoIcon } from "@/components/icons"; import { LaunchScanWorkflow } from "@/components/scans/launch-workflow"; import { SkeletonTableScans } from "@/components/scans/table"; -import { ColumnProviderScans } from "@/components/scans/table/provider-scans"; import { ColumnGetScans } from "@/components/scans/table/scans"; import { Header } from "@/components/ui"; import { DataTable } from "@/components/ui/table"; @@ -48,44 +46,11 @@ export default async function Scans({
-
- }> - - -
); } -const SSRDataTableProviders = async () => { - const providersData = await getProviders({ - filters: { "filter[connected]": "true" }, - }); - return ( - <> -
- - - -

Connected providers

-
- -

- If you don't see any providers, please check your connection settings on - the{" "} - - providers page - - . -

- - ); -}; - const SSRDataTableScans = async ({ searchParams, }: { diff --git a/components/scans/forms/schedule-form.tsx b/components/scans/forms/schedule-form.tsx index 30a8982856..2b75a3c263 100644 --- a/components/scans/forms/schedule-form.tsx +++ b/components/scans/forms/schedule-form.tsx @@ -95,7 +95,7 @@ export const ScheduleForm = ({ value !== undefined && @@ -56,7 +57,6 @@ export const LaunchScanWorkflow = ({ 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", @@ -67,6 +67,8 @@ export const LaunchScanWorkflow = ({ title: "Success!", description: "The scan was launched successfully.", }); + // Reset form after successful submission + form.reset(); } }; @@ -76,9 +78,9 @@ export const LaunchScanWorkflow = ({ onSubmit={form.handleSubmit(onSubmitClient)} className="flex flex-col space-y-4" > -
-
-
+
+
+
Launch Scan
- -
-
-
- - Cancel - - } - > - {isLoading ? <>Loading : Start now} - + + {form.watch("providerId") && ( + + + + )} + +
+ +
+ + {form.watch("providerId") && ( + + + + )} + +
+ + + {form.watch("providerId") && ( + + form.reset()} + className="w-fit border-gray-200 bg-transparent" + ariaLabel="Clear form" + variant="bordered" + size="lg" + radius="lg" + > + Cancel + + } + isDisabled={true} + > + {isLoading ? <>Loading : Schedule} + + + } + > + {isLoading ? <>Loading : Start now} + + + )} +
diff --git a/components/scans/launch-workflow/select-scan-provider.tsx b/components/scans/launch-workflow/select-scan-provider.tsx index ff9d1d0917..c1805f488b 100644 --- a/components/scans/launch-workflow/select-scan-provider.tsx +++ b/components/scans/launch-workflow/select-scan-provider.tsx @@ -58,7 +58,7 @@ export const SelectScanProvider = < aria-label="Select a Provider" placeholder="Choose a provider" labelPlacement="outside" - size="sm" + size="md" selectedKeys={field.value ? new Set([field.value]) : new Set()} onSelectionChange={(keys) => { const selectedValue = Array.from(keys)[0]?.toString(); diff --git a/components/scans/table/provider-scans/column-provider-scans.tsx b/components/scans/table/provider-scans/column-provider-scans.tsx deleted file mode 100644 index 938c3c1681..0000000000 --- a/components/scans/table/provider-scans/column-provider-scans.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; - -import { EntityInfoShort } from "@/components/ui/entities"; -import { ProviderProps } from "@/types"; - -import { DataTableRowActions } from "./data-table-row-actions"; - -const getProviderData = (row: { original: ProviderProps }) => { - return row.original; -}; - -export const ColumnProviderScans: ColumnDef[] = [ - { - accessorKey: "provider", - header: "Provider", - cell: ({ row }) => { - const { - attributes: { connection, provider, alias, uid }, - } = getProviderData(row); - return ( - - ); - }, - }, - { - accessorKey: "launchScan", - header: "Launch Scan", - cell: ({ row }) => { - return ; - }, - }, -]; diff --git a/components/scans/table/provider-scans/data-table-row-actions.tsx b/components/scans/table/provider-scans/data-table-row-actions.tsx deleted file mode 100644 index 8cad2b0f7f..0000000000 --- a/components/scans/table/provider-scans/data-table-row-actions.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client"; - -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@nextui-org/react"; -import { Row } from "@tanstack/react-table"; -import { CalendarClockIcon, RocketIcon } from "lucide-react"; -import { useState } from "react"; - -import { AddIcon } from "@/components/icons"; -import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; - -import { ScanOnDemandForm, ScheduleForm } from "../../forms"; - -interface DataTableRowActionsProps { - row: Row; -} -const iconClasses = - "text-2xl text-default-500 pointer-events-none flex-shrink-0"; - -export function DataTableRowActions({ - row, -}: DataTableRowActionsProps) { - 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 - - - - - } - onClick={() => setIsScanOnDemandOpen(true)} - > - Start now - - - - } - onClick={() => setIsScanScheduleOpen(true)} - > - Schedule Scan - - - - -
- - ); -} diff --git a/components/scans/table/provider-scans/index.ts b/components/scans/table/provider-scans/index.ts deleted file mode 100644 index 415bafad0d..0000000000 --- a/components/scans/table/provider-scans/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./column-provider-scans"; -export * from "./data-table-row-actions"; From fdb644fc6d3770aa5af9ed704842f897be43f8d8 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 8 Nov 2024 09:56:58 +0100 Subject: [PATCH 5/8] chore: WIP --- components/scans/launch-workflow/launch-scan-workflow.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/scans/launch-workflow/launch-scan-workflow.tsx b/components/scans/launch-workflow/launch-scan-workflow.tsx index f9707d3bb9..99e27bb90c 100644 --- a/components/scans/launch-workflow/launch-scan-workflow.tsx +++ b/components/scans/launch-workflow/launch-scan-workflow.tsx @@ -81,7 +81,9 @@ export const LaunchScanWorkflow = ({
- Launch Scan +

+ Launch Scan +

Date: Fri, 8 Nov 2024 10:36:28 +0100 Subject: [PATCH 6/8] feat: scan details can be shared now in the URL --- app/(prowler)/scans/page.tsx | 14 +- components/scans/forms/index.ts | 1 - .../scans/forms/scan-on-demand-form.tsx | 126 ------------------ .../scans/table/scans/column-get-scans.tsx | 6 + .../table/scans/data-table-row-details.tsx | 17 +++ components/ui/sheet/trigger-sheet.tsx | 4 +- 6 files changed, 34 insertions(+), 134 deletions(-) delete mode 100644 components/scans/forms/scan-on-demand-form.tsx diff --git a/app/(prowler)/scans/page.tsx b/app/(prowler)/scans/page.tsx index 70ded7e723..4ea3db2b76 100644 --- a/app/(prowler)/scans/page.tsx +++ b/app/(prowler)/scans/page.tsx @@ -16,10 +16,10 @@ export default async function Scans({ }: { searchParams: SearchParamsProps; }) { - const searchParamsKey = JSON.stringify(searchParams || {}); - // const providersData = await getProviders({ - // filters: { "filter[connected]": "true" }, - // }); + const filteredParams = { ...searchParams }; + delete filteredParams.scanId; + const searchParamsKey = JSON.stringify(filteredParams); + const providersData = await getProviders({}); const providerInfo = providersData?.data?.length @@ -59,9 +59,11 @@ const SSRDataTableScans = async ({ const page = parseInt(searchParams.page?.toString() || "1", 10); const sort = searchParams.sort?.toString(); - // Extract all filter parameters + // Extract all filter parameters, excluding scanId const filters = Object.fromEntries( - Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + Object.entries(searchParams).filter( + ([key]) => key.startsWith("filter[") && key !== "scanId", + ), ); // Extract query from filters diff --git a/components/scans/forms/index.ts b/components/scans/forms/index.ts index 59deb9251a..4acd302cb9 100644 --- a/components/scans/forms/index.ts +++ b/components/scans/forms/index.ts @@ -1,3 +1,2 @@ export * from "./edit-scan-form"; -export * from "./scan-on-demand-form"; export * from "./schedule-form"; diff --git a/components/scans/forms/scan-on-demand-form.tsx b/components/scans/forms/scan-on-demand-form.tsx deleted file mode 100644 index a805e55ac5..0000000000 --- a/components/scans/forms/scan-on-demand-form.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { RocketIcon } from "lucide-react"; -import { Dispatch, SetStateAction } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; - -import { scanOnDemand } from "@/actions/scans"; -import { useToast } from "@/components/ui"; -import { CustomButton, CustomInput } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; -import { onDemandScanFormSchema } from "@/types"; - -export const ScanOnDemandForm = ({ - providerId, - scanName, - scannerArgs, - setIsOpen, -}: { - providerId: string; - scanName?: string; - scannerArgs?: { checksToExecute: string[] }; - setIsOpen: Dispatch>; -}) => { - 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)} - isDisabled={isLoading} - > - Cancel - - - } - > - {isLoading ? <>Loading : Start now} - -
-
- - ); -}; diff --git a/components/scans/table/scans/column-get-scans.tsx b/components/scans/table/scans/column-get-scans.tsx index 930f189de5..3e2d940e8e 100644 --- a/components/scans/table/scans/column-get-scans.tsx +++ b/components/scans/table/scans/column-get-scans.tsx @@ -1,6 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; +import { useSearchParams } from "next/navigation"; import { InfoIcon } from "@/components/icons"; import { DateWithTime, EntityInfoShort } from "@/components/ui/entities"; @@ -125,11 +126,16 @@ export const ColumnGetScans: ColumnDef[] = [ id: "moreInfo", header: "Details", cell: ({ row }) => { + const searchParams = useSearchParams(); + const scanId = searchParams.get("scanId"); + const isOpen = scanId === row.original.id; + return ( } title="Scan Details" description="View the scan details" + defaultOpen={isOpen} > diff --git a/components/scans/table/scans/data-table-row-details.tsx b/components/scans/table/scans/data-table-row-details.tsx index 997fa0e5d9..14de7184de 100644 --- a/components/scans/table/scans/data-table-row-details.tsx +++ b/components/scans/table/scans/data-table-row-details.tsx @@ -1,5 +1,6 @@ "use client"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { getScan } from "@/actions/scans"; @@ -7,9 +8,25 @@ import { ScanDetail, SkeletonTableScans } from "@/components/scans/table"; import { ScanProps } from "@/types"; export const DataTableRowDetails = ({ entityId }: { entityId: string }) => { + const router = useRouter(); + const searchParams = useSearchParams(); const [scanDetails, setScanDetails] = useState(null); const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + // Add scanId to URL + const params = new URLSearchParams(searchParams.toString()); + params.set("scanId", entityId); + router.push(`?${params.toString()}`, { scroll: false }); + + // Cleanup function: remove scanId from URL when component unmounts + return () => { + const newParams = new URLSearchParams(searchParams.toString()); + newParams.delete("scanId"); + router.push(`?${newParams.toString()}`, { scroll: false }); + }; + }, [entityId, router, searchParams]); + useEffect(() => { const fetchScanDetails = async () => { try { diff --git a/components/ui/sheet/trigger-sheet.tsx b/components/ui/sheet/trigger-sheet.tsx index 2275577cbd..142b5a51cc 100644 --- a/components/ui/sheet/trigger-sheet.tsx +++ b/components/ui/sheet/trigger-sheet.tsx @@ -12,6 +12,7 @@ interface TriggerSheetProps { title: string; description: string; children: React.ReactNode; + defaultOpen?: boolean; } export function TriggerSheet({ @@ -19,9 +20,10 @@ export function TriggerSheet({ title, description, children, + defaultOpen = false, }: TriggerSheetProps) { return ( - + {triggerComponent} From 327b4f4bbae04f2c676bc04ce09ec22a0fa26f82 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 8 Nov 2024 10:42:31 +0100 Subject: [PATCH 7/8] chore: style tweaks for sheet component --- components/ui/sheet/trigger-sheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ui/sheet/trigger-sheet.tsx b/components/ui/sheet/trigger-sheet.tsx index 142b5a51cc..992b3ac098 100644 --- a/components/ui/sheet/trigger-sheet.tsx +++ b/components/ui/sheet/trigger-sheet.tsx @@ -27,7 +27,7 @@ export function TriggerSheet({ {triggerComponent} - + {title} {description} From bec76447988d07965b8c49662777aa911fa4436a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 8 Nov 2024 11:17:43 +0100 Subject: [PATCH 8/8] feat: render the task result in scan details --- components/scans/table/scan-detail.tsx | 59 +++++++++++++++++-- .../table/scans/data-table-row-details.tsx | 21 ++++++- types/components.ts | 16 +++++ 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/components/scans/table/scan-detail.tsx b/components/scans/table/scan-detail.tsx index 5dd8dc2547..c95e28fcfc 100644 --- a/components/scans/table/scan-detail.tsx +++ b/components/scans/table/scan-detail.tsx @@ -4,16 +4,23 @@ import { Card, CardBody, CardHeader, Divider } from "@nextui-org/react"; import { DateWithTime, SnippetId } from "@/components/ui/entities"; import { StatusBadge } from "@/components/ui/table/status-badge"; -import { ScanProps } from "@/types"; +import { ScanProps, TaskDetails } from "@/types"; -export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => { +interface ScanDetailsProps { + scanDetails: ScanProps & { + taskDetails?: TaskDetails; + }; +} + +export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => { const scanOnDemand = scanDetails.attributes; + const taskDetails = scanDetails.taskDetails; + return (
-

Scan Details -

-

{scanOnDemand.name}

+

Scan Details

@@ -22,6 +29,7 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
+ } @@ -99,7 +107,7 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
-

Scan Arguments

+

Scan arguments

@@ -115,6 +123,47 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
+ {taskDetails && ( + + +

State details

+
+ + +
+ + + + {taskDetails.attributes.result && ( + <> + + {taskDetails.attributes.result.exc_message && ( + + )} + + )} + + +
+
+
+ )}
); }; diff --git a/components/scans/table/scans/data-table-row-details.tsx b/components/scans/table/scans/data-table-row-details.tsx index 14de7184de..92adbbe18c 100644 --- a/components/scans/table/scans/data-table-row-details.tsx +++ b/components/scans/table/scans/data-table-row-details.tsx @@ -4,7 +4,9 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { getScan } from "@/actions/scans"; +import { getTask } from "@/actions/task"; import { ScanDetail, SkeletonTableScans } from "@/components/scans/table"; +import { checkTaskStatus } from "@/lib"; import { ScanProps } from "@/types"; export const DataTableRowDetails = ({ entityId }: { entityId: string }) => { @@ -31,9 +33,24 @@ export const DataTableRowDetails = ({ entityId }: { entityId: string }) => { const fetchScanDetails = async () => { try { const result = await getScan(entityId); - setScanDetails(result?.data); + + const taskId = result.data.relationships.task?.data?.id; + + if (taskId) { + const taskResult = await checkTaskStatus(taskId); + + if (taskResult.completed !== undefined) { + const task = await getTask(taskId); + setScanDetails({ + ...result.data, + taskDetails: task.data, + }); + } + } else { + setScanDetails(result.data); + } } catch (error) { - console.error("Error fetching scan details:", error); + console.error("Error in fetchScanDetails:", error); } finally { setIsLoading(false); } diff --git a/types/components.ts b/types/components.ts index b6c4b59da2..60fe123b51 100644 --- a/types/components.ts +++ b/types/components.ts @@ -26,6 +26,22 @@ export type NextUIColors = | "danger" | "default"; +export interface TaskDetails { + attributes: { + state: string; + completed_at: string; + result: { + exc_type?: string; + exc_message?: string[]; + exc_module?: string; + }; + task_args: { + scan_id: string; + provider_id: string; + checks_to_execute: string[]; + }; + }; +} export type AWSCredentials = { aws_access_key_id: string; aws_secret_access_key: string;