feat: dynamic provider selector for scan launch

This commit is contained in:
Pablo Lara
2024-11-08 08:45:27 +01:00
parent 1c37b58177
commit 5801857883
3 changed files with 203 additions and 61 deletions
+16 -4
View File
@@ -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 (
<>
<Header title="Scans" icon="lucide:scan-search" />
<Spacer y={4} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableScans />}>
<LaunchScanWorkflow />
</Suspense>
<LaunchScanWorkflow providers={providerInfo} />
<Spacer y={8} />
<div className="grid grid-cols-12 items-start gap-4">
@@ -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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerId: "",
scanName: "",
scannerArgs: { checksToExecute: [] },
},
});
const isLoading = form.formState.isSubmitting;
const onSubmitClient = async (values: z.infer<typeof formSchema>) => {
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 (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
<div className="flex flex-col gap-2">
<span className="text-sm text-default-500">Launch Scan</span>
<SelectScanProvider providers={providerInfo} />
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<div className="flex flex-col gap-4">
<div className="grid grid-cols-1 items-center gap-x-4 gap-y-4 md:grid-cols-2 xl:grid-cols-4">
<div className="flex flex-col gap-2">
<span className="text-sm text-default-500">Launch Scan</span>
<SelectScanProvider
providers={providers}
control={form.control}
name="providerId"
/>
</div>
<CustomInput
control={form.control}
name="scanName"
type="text"
label="Scan Name"
labelPlacement="outside"
placeholder="Scan Name"
variant="bordered"
isRequired={false}
isInvalid={!!form.formState.errors.scanName}
/>
</div>
</div>
</div>
</div>
<div className="flex w-full justify-center sm:space-x-6">
<CustomButton
type="button"
ariaLabel="Cancel"
className="w-full bg-transparent"
variant="faded"
size="lg"
radius="lg"
isDisabled={isLoading}
>
<span>Cancel</span>
</CustomButton>
<CustomButton
type="submit"
ariaLabel="Start scan now"
className="w-full"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
startContent={!isLoading && <RocketIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Start now</span>}
</CustomButton>
</div>
</form>
</Form>
);
};
@@ -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<TFieldValues> = FieldPath<TFieldValues>,
> {
providers: {
providerId: string;
alias: string;
providerType: string;
uid: string;
connected: boolean;
}[];
control: Control<TFieldValues>;
name: TName;
}
export const SelectScanProvider = ({ providers }: SelectScanProviderProps) => {
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
export const SelectScanProvider = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
providers,
control,
name,
}: SelectScanProviderProps<TFieldValues, TName>) => {
const renderBadge = (providerType: string) => {
switch (providerType) {
case "aws":
@@ -36,35 +48,53 @@ export const SelectScanProvider = ({ providers }: SelectScanProviderProps) => {
};
return (
<Select
aria-label="Select a Provider"
placeholder="Choose a provider"
labelPlacement="outside"
size="sm"
selectedKeys={selectedKeys}
onSelectionChange={(keys) => setSelectedKeys(new Set(keys))}
renderValue={() => {
const selectedItem = providers.find(
(item) => item.uid === Array.from(selectedKeys)[0],
);
return selectedItem ? (
<div className="flex items-center gap-2">
{renderBadge(selectedItem.providerType)}
{selectedItem.alias}
</div>
) : (
"Choose a provider"
);
}}
>
{providers.map((item) => (
<SelectItem key={item.uid} textValue={item.uid} aria-label={item.alias}>
<div className="flex items-center gap-2">
{renderBadge(item.providerType)}
{item.alias}
</div>
</SelectItem>
))}
</Select>
<FormField
control={control}
name={name}
render={({ field }) => (
<>
<FormControl>
<Select
aria-label="Select a Provider"
placeholder="Choose a provider"
labelPlacement="outside"
size="sm"
selectedKeys={field.value ? new Set([field.value]) : new Set()}
onSelectionChange={(keys) => {
const selectedValue = Array.from(keys)[0]?.toString();
field.onChange(selectedValue);
}}
renderValue={() => {
const selectedItem = providers.find(
(item) => item.providerId === field.value,
);
return selectedItem ? (
<div className="flex items-center gap-2">
{renderBadge(selectedItem.providerType)}
{selectedItem.alias}
</div>
) : (
"Choose a provider"
);
}}
>
{providers.map((item) => (
<SelectItem
key={item.providerId}
textValue={item.alias}
aria-label={item.alias}
>
<div className="flex items-center gap-2">
{renderBadge(item.providerType)}
{item.alias}
</div>
</SelectItem>
))}
</Select>
</FormControl>
<FormMessage className="text-system-error dark:text-system-error" />
</>
)}
/>
);
};