Merge pull request #82 from prowler-cloud/PRWLR-5141-Prowler-V-release-tweaks-scan-page-v2

Create workflow to launch a scan
This commit is contained in:
Pablo Lara
2024-11-08 11:21:39 +01:00
committed by GitHub
18 changed files with 441 additions and 360 deletions
+24 -41
View File
@@ -1,30 +1,43 @@
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 { FilterControls, filterScans } from "@/components/filters";
import { InfoIcon } from "@/components/icons";
import { filterScans } from "@/components/filters";
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";
import { SearchParamsProps } from "@/types";
import { ProviderProps, SearchParamsProps } from "@/types";
export default async function Scans({
searchParams,
}: {
searchParams: SearchParamsProps;
}) {
const searchParamsKey = JSON.stringify(searchParams || {});
const filteredParams = { ...searchParams };
delete filteredParams.scanId;
const searchParamsKey = JSON.stringify(filteredParams);
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} />
<FilterControls search date providers />
<LaunchScanWorkflow providers={providerInfo} />
<Spacer y={8} />
<div className="grid grid-cols-12 items-start gap-4">
@@ -33,43 +46,11 @@ export default async function Scans({
<SSRDataTableScans searchParams={searchParams} />
</Suspense>
</div>
<div className="col-span-12 lg:col-span-6">
<Suspense key={searchParamsKey} fallback={<SkeletonTableScans />}>
<SSRDataTableProviders />
</Suspense>
</div>
</div>
</>
);
}
const SSRDataTableProviders = async () => {
const filters = { "filter[connected]": "true" };
const providersData = await getProviders({ page: 1, filters });
return (
<>
<div className="mb-2 flex items-center gap-2">
<Tooltip content="Only connected providers can be scanned">
<InfoIcon size={16} />
</Tooltip>
<p className="text-sm text-default-500">Connected providers</p>
</div>
<DataTable
columns={ColumnProviderScans}
data={providersData?.data || []}
/>
<p className="-mt-4 text-sm text-default-500">
If you don't see any providers, please check your connection settings on
the{" "}
<Link className="text-sm font-medium" href="/providers">
providers page
</Link>
.
</p>
</>
);
};
const SSRDataTableScans = async ({
searchParams,
}: {
@@ -78,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
+5
View File
@@ -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",
-1
View File
@@ -1,3 +1,2 @@
export * from "./edit-scan-form";
export * from "./scan-on-demand-form";
export * from "./schedule-form";
@@ -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<SetStateAction<boolean>>;
}) => {
const formSchema = onDemandScanFormSchema();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerId: providerId,
scanName: scanName,
scannerArgs: scannerArgs,
},
});
const { toast } = useToast();
const isLoading = form.formState.isSubmitting;
const onSubmitClient = async (values: z.infer<typeof formSchema>) => {
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<input type="hidden" name="providerId" value={providerId} />
<div>
<CustomInput
control={form.control}
name="scanName"
type="text"
label="Scan Name"
labelPlacement="outside"
placeholder={scanName}
variant="bordered"
isRequired={false}
isInvalid={!!form.formState.errors.scanName}
/>
</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"
onPress={() => setIsOpen(false)}
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 -1
View File
@@ -95,7 +95,7 @@ export const ScheduleForm = ({
<CustomButton
type="submit"
ariaLabel="Save"
ariaLabel="Schedule scan"
className="w-full"
variant="solid"
color="action"
@@ -0,0 +1,2 @@
export * from "./launch-scan-workflow";
export * from "./select-scan-provider";
@@ -0,0 +1,193 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { scanOnDemand } from "@/actions/scans";
import { RocketIcon, ScheduleIcon } 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";
type ProviderInfo = {
providerId: string;
alias: string;
providerType: string;
uid: string;
connected: boolean;
};
export const LaunchScanWorkflow = ({
providers,
}: {
providers: ProviderInfo[];
}) => {
const formSchema = onDemandScanFormSchema();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerId: "",
scanName: "",
scannerArgs: undefined,
},
});
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
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}`;
toast({
variant: "destructive",
title: "Oops! Something went wrong",
description: errorMessage,
});
} else {
toast({
title: "Success!",
description: "The scan was launched successfully.",
});
// Reset form after successful submission
form.reset();
}
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<div className="grid grid-cols-2 gap-6">
<div className="flex flex-col gap-6">
<div className="w-full">
<p className="pb-1 text-sm font-medium text-default-700">
Launch Scan
</p>
<SelectScanProvider
providers={providers}
control={form.control}
name="providerId"
/>
</div>
<AnimatePresence>
{form.watch("providerId") && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<CustomInput
control={form.control}
name="scanName"
type="text"
label="Scan Name (optional)"
labelPlacement="outside"
placeholder="Scan Name"
variant="bordered"
isRequired={false}
isInvalid={!!form.formState.errors.scanName}
/>
</motion.div>
)}
</AnimatePresence>
</div>
<div className="flex flex-col justify-start">
<AnimatePresence>
{form.watch("providerId") && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
<CustomInput
control={form.control}
name="scannerArgs"
type="text"
label="Scanner Args (optional)"
labelPlacement="outside"
placeholder="Scanner Args"
variant="bordered"
isRequired={false}
isInvalid={!!form.formState.errors.scannerArgs}
/>
</motion.div>
)}
</AnimatePresence>
</div>
<AnimatePresence>
{form.watch("providerId") && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
className="col-span-2 flex justify-start gap-4"
>
<CustomButton
onPress={() => form.reset()}
className="w-fit border-gray-200 bg-transparent"
ariaLabel="Clear form"
variant="bordered"
size="lg"
radius="lg"
>
Cancel
</CustomButton>
<CustomButton
type="submit"
ariaLabel="Schedule scan"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
startContent={!isLoading && <ScheduleIcon size={24} />}
isDisabled={true}
>
{isLoading ? <>Loading</> : <span>Schedule</span>}
</CustomButton>
<CustomButton
type="submit"
ariaLabel="Start scan now"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
startContent={!isLoading && <RocketIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Start now</span>}
</CustomButton>
</motion.div>
)}
</AnimatePresence>
</div>
</form>
</Form>
);
};
@@ -0,0 +1,100 @@
"use client";
import { Select, SelectItem } from "@nextui-org/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<
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 = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
providers,
control,
name,
}: SelectScanProviderProps<TFieldValues, TName>) => {
const renderBadge = (providerType: string) => {
switch (providerType) {
case "aws":
return <AWSProviderBadge width={25} height={25} />;
case "azure":
return <AzureProviderBadge width={25} height={25} />;
case "gcp":
return <GCPProviderBadge width={25} height={25} />;
case "kubernetes":
return <KS8ProviderBadge width={25} height={25} />;
default:
return null;
}
};
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<>
<FormControl>
<Select
aria-label="Select a Provider"
placeholder="Choose a provider"
labelPlacement="outside"
size="md"
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" />
</>
)}
/>
);
};
@@ -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<ProviderProps>[] = [
{
accessorKey: "provider",
header: "Provider",
cell: ({ row }) => {
const {
attributes: { connection, provider, alias, uid },
} = getProviderData(row);
return (
<EntityInfoShort
connected={connection.connected}
cloudProvider={provider}
entityAlias={alias}
entityId={uid}
/>
);
},
},
{
accessorKey: "launchScan",
header: "Launch Scan",
cell: ({ row }) => {
return <DataTableRowActions row={row} />;
},
},
];
@@ -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<ProviderProps> {
row: Row<ProviderProps>;
}
const iconClasses =
"text-2xl text-default-500 pointer-events-none flex-shrink-0";
export function DataTableRowActions<ProviderProps>({
row,
}: DataTableRowActionsProps<ProviderProps>) {
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 (
<>
<CustomAlertModal
isOpen={isScanOnDemandOpen}
onOpenChange={setIsScanOnDemandOpen}
title="Start Scan On Demand"
description={"Start a scan on demand for this provider"}
>
<ScanOnDemandForm
providerId={providerId}
scanName={scanName}
setIsOpen={setIsScanOnDemandOpen}
/>
</CustomAlertModal>
<CustomAlertModal
isOpen={isScanScheduleOpen}
onOpenChange={setIsScanScheduleOpen}
title="Schedule Scan"
description={"Schedule a scan for this provider"}
>
<ScheduleForm
providerId={providerId}
scheduleDate={""}
setIsOpen={setIsScanScheduleOpen}
/>
</CustomAlertModal>
<div className="relative flex items-center justify-end gap-2">
<Dropdown className="shadow-xl" placement="bottom-start">
<DropdownTrigger>
<CustomButton
className="w-full"
ariaLabel="Start Scan"
variant="solid"
color="action"
size="md"
endContent={<AddIcon size={20} />}
>
Start
</CustomButton>
</DropdownTrigger>
<DropdownMenu
closeOnSelect
aria-label="Launch Scan"
color="default"
variant="flat"
>
<DropdownSection title="Start Scan On Demand">
<DropdownItem
key="scanNow"
color="primary"
description="Allows you to start a scan on demand"
textValue="Start now"
startContent={<RocketIcon className={iconClasses} />}
onClick={() => setIsScanOnDemandOpen(true)}
>
Start now
</DropdownItem>
</DropdownSection>
<DropdownSection title="Schedule Scan">
<DropdownItem
key="schedule"
color="primary"
description="Schedule a scan for this provider"
textValue="Schedule Scan"
startContent={<CalendarClockIcon className={iconClasses} />}
onClick={() => setIsScanScheduleOpen(true)}
>
Schedule Scan
</DropdownItem>
</DropdownSection>
</DropdownMenu>
</Dropdown>
</div>
</>
);
}
@@ -1,2 +0,0 @@
export * from "./column-provider-scans";
export * from "./data-table-row-actions";
+54 -5
View File
@@ -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 (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex flex-col items-baseline md:flex-row md:gap-x-4">
<h2 className="text-lg font-black uppercase">Scan Details - </h2>
<p>{scanOnDemand.name}</p>
<h2 className="text-2xl font-bold">Scan Details</h2>
</div>
<StatusBadge size="lg" status={scanOnDemand.state} />
@@ -22,6 +29,7 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
<div className="relative z-0 flex w-full flex-col justify-between gap-4 overflow-auto rounded-large bg-content1 p-4 shadow-small">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="space-y-4">
<DetailItem label="Scan Name" value={scanOnDemand.name} />
<DetailItem
label="ID"
value={<SnippetId label="Type" entityId={scanDetails.id} />}
@@ -99,7 +107,7 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
</div>
<Card className="relative w-full border-small border-default-100 p-3 shadow-lg">
<CardHeader className="py-2">
<h2 className="text-2xl font-bold">Scan Arguments</h2>
<h2 className="text-2xl font-bold">Scan arguments</h2>
</CardHeader>
<Divider />
@@ -115,6 +123,47 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
</div>
</CardBody>
</Card>
{taskDetails && (
<Card className="relative w-full border-small border-default-100 p-3 shadow-lg">
<CardHeader className="py-2">
<h2 className="text-2xl font-bold">State details</h2>
</CardHeader>
<Divider />
<CardBody className="p-4">
<div className="flex flex-col gap-2">
<DetailItem label="State" value={taskDetails.attributes.state} />
<DetailItem
label="Completed At"
value={taskDetails.attributes.completed_at}
/>
{taskDetails.attributes.result && (
<>
<DetailItem
label="Error Type"
value={taskDetails.attributes.result.exc_type}
/>
{taskDetails.attributes.result.exc_message && (
<DetailItem
label="Error Message"
value={taskDetails.attributes.result.exc_message.join(
", ",
)}
/>
)}
</>
)}
<DetailItem
label="Checks to Execute"
value={taskDetails.attributes.task_args.checks_to_execute.join(
", ",
)}
/>
</div>
</CardBody>
</Card>
)}
</div>
);
};
@@ -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<ScanProps>[] = [
id: "moreInfo",
header: "Details",
cell: ({ row }) => {
const searchParams = useSearchParams();
const scanId = searchParams.get("scanId");
const isOpen = scanId === row.original.id;
return (
<TriggerSheet
triggerComponent={<InfoIcon className="text-primary" size={16} />}
title="Scan Details"
description="View the scan details"
defaultOpen={isOpen}
>
<DataTableRowDetails entityId={row.original.id} />
</TriggerSheet>
@@ -1,22 +1,56 @@
"use client";
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 }) => {
const router = useRouter();
const searchParams = useSearchParams();
const [scanDetails, setScanDetails] = useState<ScanProps | null>(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 {
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);
}
@@ -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 (
<Button
className={clsx("m-0 h-fit min-w-min border-none bg-transparent p-0", {
"text-danger": danger,
})}
spinner={<CircularProgress aria-label="Loading..." size="sm" />}
type="submit"
area-disabled={pending}
>
{buttonLabel}
</Button>
);
};
-1
View File
@@ -5,4 +5,3 @@ export * from "./custom-dropdown-filter";
export * from "./custom-input";
export * from "./custom-loader";
export * from "./custom-radio";
export * from "./CustomButtonClientAction";
+4 -2
View File
@@ -12,6 +12,7 @@ interface TriggerSheetProps {
title: string;
description: string;
children: React.ReactNode;
defaultOpen?: boolean;
}
export function TriggerSheet({
@@ -19,13 +20,14 @@ export function TriggerSheet({
title,
description,
children,
defaultOpen = false,
}: TriggerSheetProps) {
return (
<Sheet>
<Sheet defaultOpen={defaultOpen}>
<SheetTrigger className="flex items-center gap-2">
{triggerComponent}
</SheetTrigger>
<SheetContent className="max-w-[95vw] pt-10 md:max-w-[45vw]">
<SheetContent className="my-4 max-h-[calc(100vh-2rem)] max-w-[95vw] rounded-l-xl pt-10 md:my-8 md:max-h-[calc(100vh-4rem)] md:max-w-[55vw]">
<SheetHeader>
<SheetTitle className="sr-only">{title}</SheetTitle>
<SheetDescription className="sr-only">{description}</SheetDescription>
+16
View File
@@ -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;