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/index.ts b/actions/scans/index.ts new file mode 100644 index 0000000000..7e4b1aceeb --- /dev/null +++ b/actions/scans/index.ts @@ -0,0 +1 @@ +export * from "./scans"; diff --git a/actions/scans/scans.ts b/actions/scans/scans.ts new file mode 100644 index 0000000000..be481fc2ea --- /dev/null +++ b/actions/scans/scans.ts @@ -0,0 +1,159 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { auth } from "@/auth.config"; +import { getErrorMessage, parseStringify } from "@/lib"; + +export const getScans = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}) => { + const session = await auth(); + + if (isNaN(Number(page)) || page < 1) redirect("/scans"); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/scans`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const scans = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const data = await scans.json(); + const parsedData = parseStringify(data); + revalidatePath("/scans"); + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching scans:", error); + return undefined; + } +}; + +export const getScan = async (scanId: string) => { + const session = await auth(); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/scans/${scanId}`); + + try { + const scan = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const data = await scan.json(); + const parsedData = parseStringify(data); + + return parsedData; + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; + +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), + }; + } +}; + +export const updateScan = async (formData: FormData) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + + const scanId = formData.get("scanId"); + const scanName = formData.get("scanName"); + + const url = new URL(`${keyServer}/scans/${scanId}`); + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers: { + "Content-Type": "application/vnd.api+json", + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + body: JSON.stringify({ + data: { + type: "Scan", + id: scanId, + attributes: { + name: scanName, + }, + }, + }), + }); + const data = await response.json(); + revalidatePath("/scans"); + return parseStringify(data); + } catch (error) { + console.error(error); + return { + error: getErrorMessage(error), + }; + } +}; diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index abc0ccfa59..02860e2a0b 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -24,7 +24,7 @@ export default async function Providers({
- + @@ -60,6 +60,7 @@ const SSRDataTable = async ({ columns={ColumnProviders} data={providersData?.data || []} metadata={providersData?.meta} + customFilters={filterProviders} /> ); }; diff --git a/app/(prowler)/scans/page.tsx b/app/(prowler)/scans/page.tsx index f9cbb862cd..d696bdc9a0 100644 --- a/app/(prowler)/scans/page.tsx +++ b/app/(prowler)/scans/page.tsx @@ -1,54 +1,133 @@ import { Spacer } from "@nextui-org/react"; +import { Suspense } from "react"; -// import { Suspense } from "react"; -// import { getProviders } from "@/actions/providers"; -import { FilterControls, filterScans } from "@/components/filters"; -// import { ColumnScans, SkeletonTableScans } from "@/components/scans/table"; +import { getProviders } from "@/actions/providers"; +import { getScans } from "@/actions/scans"; +import { + FilterControls, + filterProviders, + filterScans, +} from "@/components/filters"; +import { SkeletonTableScans } from "@/components/scans/table"; +import { ColumnProviderScans } from "@/components/scans/table/provider-scans"; +import { ColumnGetScans } from "@/components/scans/table/scans"; +import { ColumnGetScansSchedule } from "@/components/scans/table/schedule-scans"; import { Header } from "@/components/ui"; -// import { DataTable } from "@/components/ui/table"; -// import { SearchParamsProps } from "@/types"; +import { DataTable } from "@/components/ui/table"; +import { SearchParamsProps } from "@/types"; -export default async function Scans() { - // const searchParamsKey = JSON.stringify(searchParams || {}); +export default async function Scans({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { + const searchParamsKey = JSON.stringify(searchParams || {}); return ( <>
- - + + - {/* }> - - */} +
+
+ }> + + +
+
+ }> + + +
+
+ }> + + +
+
); } -// const SSRDataTable = async ({ -// searchParams, -// }: { -// searchParams: SearchParamsProps; -// }) => { -// const page = parseInt(searchParams.page?.toString() || "1", 10); -// const sort = searchParams.sort?.toString(); +const SSRDataTableProviders = async () => { + // const page = parseInt(searchParams.page?.toString() || "1", 10); + // const sort = searchParams.sort?.toString(); -// // Extract all filter parameters -// const filters = Object.fromEntries( -// Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), -// ); + // Extract all filter parameters + // const filters = Object.fromEntries( + // Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + // ); -// // Extract query from filters -// const query = (filters["filter[search]"] as string) || ""; + // Extract query from filters + // const query = (filters["filter[search]"] as string) || ""; -// const providersData = await getProviders({ query, page, sort, filters }); + const providersData = await getProviders({ page: 1 }); -// return ( -// -// ); -// }; + return ( + + ); +}; + +const SSRDataTableScansSchedule = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString(); + + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + // Extract query from filters + const query = (filters["filter[search]"] as string) || ""; + + const scansData = await getScans({ query, page, sort, filters }); + + return ( + + ); +}; + +const SSRDataTableScans = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString(); + + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + // Extract query from filters + const query = (filters["filter[search]"] as string) || ""; + + const scansData = await getScans({ query, page, sort, filters }); + + return ( + + ); +}; diff --git a/components/auth/oss/auth-form.tsx b/components/auth/oss/auth-form.tsx index bb188bdf96..e8c997f6a9 100644 --- a/components/auth/oss/auth-form.tsx +++ b/components/auth/oss/auth-form.tsx @@ -71,7 +71,6 @@ export const AuthForm = ({ type }: { type: string }) => { if (newUser?.errors && newUser.errors.length > 0) { newUser.errors.forEach((error: ApiError) => { const errorMessage = error.detail; - switch (error.source.pointer) { case "/data/attributes/name": form.setError("name", { type: "server", message: errorMessage }); @@ -79,6 +78,12 @@ export const AuthForm = ({ type }: { type: string }) => { case "/data/attributes/email": form.setError("email", { type: "server", message: errorMessage }); break; + case "/data/attributes/company_name": + form.setError("company", { + type: "server", + message: errorMessage, + }); + break; case "/data/attributes/password": form.setError("password", { type: "server", @@ -234,7 +239,7 @@ export const AuthForm = ({ type }: { type: string }) => { size="md" radius="md" isLoading={isLoading} - disabled={isLoading} + isDisabled={isLoading} > {isLoading ? ( Loading diff --git a/components/filters/data-filters.ts b/components/filters/data-filters.ts index 81bbf0d852..d6d876b522 100644 --- a/components/filters/data-filters.ts +++ b/components/filters/data-filters.ts @@ -8,9 +8,21 @@ export const filterProviders = [ ]; export const filterScans = [ + { + key: "state", + labelCheckboxGroup: "State", + values: [ + "available", + "scheduled", + "executing", + "completed", + "failed", + "cancelled", + ], + }, { key: "trigger", - labelCheckboxGroup: "Scan Schedule", + labelCheckboxGroup: "Schedule", values: ["scheduled", "manual"], }, // Add more filter categories as needed diff --git a/components/filters/filter-controls.tsx b/components/filters/filter-controls.tsx index 19053cb62b..28c455ceea 100644 --- a/components/filters/filter-controls.tsx +++ b/components/filters/filter-controls.tsx @@ -6,8 +6,8 @@ import React, { useCallback, useEffect, useState } from "react"; import { FilterControlsProps } from "@/types"; import { CrossIcon } from "../icons"; -import { DataTableFilterCustom } from "../providers/table"; import { CustomButton } from "../ui/custom"; +import { DataTableFilterCustom } from "../ui/table"; import { CustomCheckboxMutedFindings } from "./custo-checkbox-muted-findings"; import { CustomAccountSelection } from "./custom-account-selection"; import { CustomDatePicker } from "./custom-date-picker"; @@ -22,7 +22,7 @@ export const FilterControls: React.FC = ({ regions = false, accounts = false, mutedFindings = false, - customFilters = [], + customFilters, }) => { const router = useRouter(); const searchParams = useSearchParams(); @@ -69,7 +69,7 @@ export const FilterControls: React.FC = ({ )} - + {customFilters && } ); }; diff --git a/components/findings/table/ColumnsFindings.tsx b/components/findings/table/ColumnsFindings.tsx index c8fef32133..b27f4a6dbb 100644 --- a/components/findings/table/ColumnsFindings.tsx +++ b/components/findings/table/ColumnsFindings.tsx @@ -10,7 +10,7 @@ import { import { ColumnDef } from "@tanstack/react-table"; import { VerticalDotsIcon } from "@/components/icons"; -import { SeverityBadge, StatusBadge } from "@/components/ui/table"; +import { SeverityBadge } from "@/components/ui/table"; import { FindingProps } from "@/types"; const getFindingsAttributes = (row: { original: FindingProps }) => { @@ -34,14 +34,7 @@ export const ColumnsFindings: ColumnDef[] = [ return ; }, }, - { - accessorKey: "status", - header: "Status", - cell: ({ row }) => { - const { status } = getFindingsAttributes(row); - return ; - }, - }, + { accessorKey: "region", header: "Region", diff --git a/components/icons/Icons.tsx b/components/icons/Icons.tsx index 2816f0dd74..6d886cf788 100644 --- a/components/icons/Icons.tsx +++ b/components/icons/Icons.tsx @@ -475,16 +475,13 @@ export const IdIcon: React.FC = ({ }) => ( - + ); @@ -578,6 +575,89 @@ export const ConnectionIcon: React.FC = ({ ); }; +export const ConnectionTrue: React.FC = ({ + size = 24, + width, + height, + ...props +}) => ( + + + +); + +export const ConnectionFalse: React.FC = ({ + size = 24, + width, + height, + ...props +}) => ( + + + +); + +export const ConnectionPending: React.FC = ({ + size = 24, + width, + height, + ...props +}) => ( + + + + + + + + +); + export const SuccessIcon: React.FC = ({ size = 24, width, diff --git a/components/providers/forms/add-form.tsx b/components/providers/forms/add-form.tsx index c721a709c0..3529995cfa 100644 --- a/components/providers/forms/add-form.tsx +++ b/components/providers/forms/add-form.tsx @@ -103,7 +103,7 @@ export const AddForm = ({ size="lg" radius="lg" onPress={() => setIsOpen(false)} - disabled={isLoading} + isDisabled={isLoading} > Cancel diff --git a/components/providers/forms/delete-form.tsx b/components/providers/forms/delete-form.tsx index e08ac4ada5..2f80b3294b 100644 --- a/components/providers/forms/delete-form.tsx +++ b/components/providers/forms/delete-form.tsx @@ -63,7 +63,7 @@ export const DeleteForm = ({ size="lg" radius="lg" onPress={() => setIsOpen(false)} - disabled={isLoading} + isDisabled={isLoading} > Cancel diff --git a/components/providers/forms/edit-form.tsx b/components/providers/forms/edit-form.tsx index bae117b7da..fba187a22f 100644 --- a/components/providers/forms/edit-form.tsx +++ b/components/providers/forms/edit-form.tsx @@ -77,7 +77,7 @@ export const EditForm = ({ name="alias" type="text" label="Alias" - labelPlacement="inside" + labelPlacement="outside" placeholder={providerAlias} variant="bordered" isRequired={false} @@ -95,7 +95,7 @@ export const EditForm = ({ size="lg" radius="lg" onPress={() => setIsOpen(false)} - disabled={isLoading} + isDisabled={isLoading} > Cancel diff --git a/components/providers/index.ts b/components/providers/index.ts index 9f2aed9e3a..5093cbf0ac 100644 --- a/components/providers/index.ts +++ b/components/providers/index.ts @@ -1,8 +1,5 @@ export * from "./add-provider"; export * from "./CheckConnectionProvider"; -export * from "./date-with-time"; export * from "./forms/delete-form"; export * from "./provider-info"; export * from "./radio-group-provider"; -export * from "./scan-status"; -export * from "./snippet-id-provider"; diff --git a/components/providers/provider-info.tsx b/components/providers/provider-info.tsx index 7ff3a52821..9d6a11c371 100644 --- a/components/providers/provider-info.tsx +++ b/components/providers/provider-info.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { ConnectionIcon } from "../icons"; +import { ConnectionFalse, ConnectionPending, ConnectionTrue } from "../icons"; import { AWSProviderBadge, AzureProviderBadge, @@ -23,24 +23,24 @@ export const ProviderInfo: React.FC = ({ switch (connected) { case true: return ( -
- +
+
); case false: return ( -
- +
+
); case null: return (
- +
); default: - return ; + return ; } }; @@ -60,13 +60,15 @@ export const ProviderInfo: React.FC = ({ }; return ( -
+
{getProviderLogo()}
{getIcon()}
- {providerAlias} + + {providerAlias} + {/* */}
diff --git a/components/providers/snippet-id-provider.tsx b/components/providers/snippet-id-provider.tsx deleted file mode 100644 index 820b08bd6d..0000000000 --- a/components/providers/snippet-id-provider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Snippet } from "@nextui-org/react"; -import React from "react"; - -import { CopyIcon, DoneIcon, IdIcon } from "../icons"; - -interface SnippetIdProviderProps { - providerId: string; -} -export const SnippetIdProvider: React.FC = ({ - providerId, -}) => { - return ( - } - checkIcon={} - > -

- - - {providerId} - -

-
- ); -}; diff --git a/components/providers/table/column-providers.tsx b/components/providers/table/column-providers.tsx index 4ba3e7d0ff..1680fd8000 100644 --- a/components/providers/table/column-providers.tsx +++ b/components/providers/table/column-providers.tsx @@ -1,14 +1,12 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { add } from "date-fns"; +import { DateWithTime, SnippetId } from "@/components/ui/entities"; import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; import { ProviderProps } from "@/types"; -import { DateWithTime } from "../date-with-time"; import { ProviderInfo } from "../provider-info"; -import { SnippetIdProvider } from "../snippet-id-provider"; import { DataTableRowActions } from "./data-table-row-actions"; const getProviderData = (row: { original: ProviderProps }) => { @@ -16,10 +14,10 @@ const getProviderData = (row: { original: ProviderProps }) => { }; export const ColumnProviders: ColumnDef[] = [ - { - header: " ", - cell: ({ row }) =>

{row.index + 1}

, - }, + // { + // header: " ", + // cell: ({ row }) =>

{row.index + 1}

, + // }, { accessorKey: "account", header: ({ column }) => ( @@ -47,7 +45,7 @@ export const ColumnProviders: ColumnDef[] = [ const { attributes: { uid }, } = getProviderData(row); - return ; + return ; }, }, { @@ -74,27 +72,6 @@ export const ColumnProviders: ColumnDef[] = [ return ; }, }, - { - accessorKey: "nextScan", - header: "Next Scan", - cell: ({ row }) => { - const { - attributes: { updated_at }, - } = getProviderData(row); - const nextDay = add(new Date(updated_at), { - hours: 24, - }); - return ; - }, - }, - { - accessorKey: "resources", - header: "Resources", - cell: () => { - // Temporarily overwriting the value until the API is functional. - return

{288}

; - }, - }, { accessorKey: "added", header: ({ column }) => ( 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 = editScanFormSchema(scanName); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + scanId: scanId, + scanName: scanName, + }, + }); + + 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 updateScan(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 updated successfully.", + }); + setIsOpen(false); // Close the modal on success + } + }; + + return ( +
+ +
+ Current name: {scanName} +
+
+ +
+ + +
+ setIsOpen(false)} + isDisabled={isLoading} + > + Cancel + + + } + > + {isLoading ? <>Loading : Save} + +
+
+ + ); +}; diff --git a/components/scans/forms/index.ts b/components/scans/forms/index.ts new file mode 100644 index 0000000000..59deb9251a --- /dev/null +++ b/components/scans/forms/index.ts @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000000..a805e55ac5 --- /dev/null +++ b/components/scans/forms/scan-on-demand-form.tsx @@ -0,0 +1,126 @@ +"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/forms/schedule-form.tsx b/components/scans/forms/schedule-form.tsx new file mode 100644 index 0000000000..30a8982856 --- /dev/null +++ b/components/scans/forms/schedule-form.tsx @@ -0,0 +1,113 @@ +"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)} + isDisabled={isLoading} + > + Cancel + + + } + isDisabled={true} + > + {isLoading ? <>Loading : Schedule} + +
+ + + ); +}; diff --git a/components/scans/table/column-scans.tsx b/components/scans/table/column-scans.tsx deleted file mode 100644 index 401f02aa14..0000000000 --- a/components/scans/table/column-scans.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { add } from "date-fns"; - -import { - DateWithTime, - ProviderInfo, - SnippetIdProvider, -} from "@/components/providers"; -import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; -import { ProviderProps } from "@/types"; - -import { DataTableRowActions } from "./data-table-row-actions"; - -const getProviderData = (row: { original: ProviderProps }) => { - return row.original; -}; - -export const ColumnScans: ColumnDef[] = [ - { - header: " ", - cell: ({ row }) =>

{row.index + 1}

, - }, - { - accessorKey: "account", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { connection, provider, alias }, - } = getProviderData(row); - return ( - - ); - }, - }, - { - accessorKey: "uid", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { uid }, - } = getProviderData(row); - return ; - }, - }, - { - accessorKey: "status", - header: "Scan Status", - cell: () => { - // Temporarily overwriting the value until the API is functional. - return ; - }, - }, - { - accessorKey: "lastScan", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { updated_at }, - } = getProviderData(row); - return ; - }, - }, - { - accessorKey: "nextScan", - header: "Next Scan", - cell: ({ row }) => { - const { - attributes: { updated_at }, - } = getProviderData(row); - const nextDay = add(new Date(updated_at), { - hours: 24, - }); - return ; - }, - }, - { - accessorKey: "resources", - header: "Resources", - cell: () => { - // Temporarily overwriting the value until the API is functional. - return

{288}

; - }, - }, - { - accessorKey: "added", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { inserted_at }, - } = getProviderData(row); - return ; - }, - }, - { - id: "actions", - cell: ({ row }) => { - return ; - }, - }, -]; diff --git a/components/scans/table/data-table-filter-custom.tsx b/components/scans/table/data-table-filter-custom.tsx deleted file mode 100644 index 5dc2f94dc2..0000000000 --- a/components/scans/table/data-table-filter-custom.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { useRouter, useSearchParams } from "next/navigation"; -import React, { useState } from "react"; -import { useCallback } from "react"; - -import { CustomFilterIcon } from "@/components/icons"; -import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom"; -import { FilterOption } from "@/types"; - -export interface DataTableFilterCustomProps { - filters: FilterOption[]; -} - -export const DataTableFilterCustom = ({ - filters, -}: DataTableFilterCustomProps) => { - const router = useRouter(); - const searchParams = useSearchParams(); - const [showFilters, setShowFilters] = useState(false); - - const pushDropdownFilter = useCallback( - (key: string, values: string[]) => { - const params = new URLSearchParams(searchParams); - const filterKey = `filter[${key}]`; - - if (values.length === 0) { - params.delete(filterKey); - } else { - params.set(filterKey, values.join(",")); - } - - router.push(`?${params.toString()}`); - }, - [router, searchParams], - ); - - return ( -
- } - onPress={() => setShowFilters(!showFilters)} - > -

- {showFilters ? "Hide Filters" : "Show Filters"} -

-
- -
-
- {filters.map((filter) => ( - - ))} -
-
-
- ); -}; diff --git a/components/scans/table/data-table-row-actions.tsx b/components/scans/table/data-table-row-actions.tsx deleted file mode 100644 index fdb4f2d3b0..0000000000 --- a/components/scans/table/data-table-row-actions.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { - Button, - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@nextui-org/react"; -import { - AddNoteBulkIcon, - DeleteDocumentBulkIcon, - EditDocumentBulkIcon, -} from "@nextui-org/shared-icons"; -import { Row } from "@tanstack/react-table"; -import clsx from "clsx"; -import { useState } from "react"; - -import { VerticalDotsIcon } from "@/components/icons"; -import { CustomAlertModal } from "@/components/ui/custom"; - -// import { EditForm } from "../forms"; -// import { DeleteForm } from "../forms/delete-form"; - -interface DataTableRowActionsProps { - row: Row; -} -const iconClasses = - "text-2xl text-default-500 pointer-events-none flex-shrink-0"; - -export function DataTableRowActions({ - row, -}: DataTableRowActionsProps) { - const [isEditOpen, setIsEditOpen] = useState(false); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const providerId = (row.original as { id: string }).id; - // const providerAlias = (row.original as any).attributes?.alias; - return ( - <> - -

Hello

- {/* */} -
- -

Hello

- {/* */} -
- -
- - - - - - - } - > -

action here + {providerId}

- {/* */} -
- } - onClick={() => setIsEditOpen(true)} - > - Edit Provider - -
- - - } - onClick={() => setIsDeleteOpen(true)} - > - Delete Provider - - -
-
-
- - ); -} diff --git a/components/scans/table/index.ts b/components/scans/table/index.ts index 1d24d2e598..0a8aee6de4 100644 --- a/components/scans/table/index.ts +++ b/components/scans/table/index.ts @@ -1,4 +1,2 @@ -export * from "./column-scans"; -export * from "./data-table-filter-custom"; -export * from "./data-table-row-actions"; +export * from "./scan-detail"; export * from "./skeleton-table-scans"; diff --git a/components/scans/table/provider-scans/column-provider-scans.tsx b/components/scans/table/provider-scans/column-provider-scans.tsx new file mode 100644 index 0000000000..938c3c1681 --- /dev/null +++ b/components/scans/table/provider-scans/column-provider-scans.tsx @@ -0,0 +1,39 @@ +"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 new file mode 100644 index 0000000000..8cad2b0f7f --- /dev/null +++ b/components/scans/table/provider-scans/data-table-row-actions.tsx @@ -0,0 +1,110 @@ +"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 new file mode 100644 index 0000000000..415bafad0d --- /dev/null +++ b/components/scans/table/provider-scans/index.ts @@ -0,0 +1,2 @@ +export * from "./column-provider-scans"; +export * from "./data-table-row-actions"; diff --git a/components/scans/table/scan-detail.tsx b/components/scans/table/scan-detail.tsx new file mode 100644 index 0000000000..f9c14384ee --- /dev/null +++ b/components/scans/table/scan-detail.tsx @@ -0,0 +1,142 @@ +"use client"; + +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"; + +export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => { + const scanOnDemand = scanDetails.attributes; + return ( +
+
+
+

Scan Details -

+

{scanOnDemand.name}

+
+ + +
+ +
+
+
+ } + /> + + + + +
+
+ + ) : ( + "Not Started" + ) + } + /> + + ) : ( + "Not Started" + ) + } + /> + + ) : ( + "Not Scheduled" + ) + } + /> + + } + /> + + } + /> +
+
+
+ + +

Scan Arguments

+
+ + + + + + +
+
+ ); +}; + +const DateItem = ({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) => ( +
+ {label}: + {value} +
+); + +const DetailItem = ({ + label, + value, +}: { + label: string; + value: React.ReactNode; +}) => ( +
+ {label}: + {value} +
+); diff --git a/components/scans/table/scans/column-get-scans.tsx b/components/scans/table/scans/column-get-scans.tsx new file mode 100644 index 0000000000..e8acabb434 --- /dev/null +++ b/components/scans/table/scans/column-get-scans.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { DateWithTime, EntityInfoShort } from "@/components/ui/entities"; +import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; +import { ScanProps } from "@/types"; + +import { DataTableRowActions } from "./data-table-row-actions"; + +const getScanData = (row: { original: ScanProps }) => { + return row.original; +}; + +export const ColumnGetScans: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { name }, + } = getScanData(row); + return ; + }, + }, + { + accessorKey: "trigger", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { trigger }, + } = getScanData(row); + return

{trigger}

; + }, + }, + + { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { state }, + } = getScanData(row); + return ; + }, + }, + { + accessorKey: "scheduled_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { scheduled_at }, + } = getScanData(row); + return ; + }, + }, + { + accessorKey: "started_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { started_at }, + } = getScanData(row); + return ; + }, + }, + { + accessorKey: "completed_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { completed_at }, + } = getScanData(row); + return ; + }, + }, + { + accessorKey: "scanner_args", + header: "Scanner Args", + cell: ({ row }) => { + const { + attributes: { scanner_args }, + } = getScanData(row); + return

{scanner_args?.only_logs}

; + }, + }, + { + accessorKey: "resources", + header: "Resources", + cell: ({ row }) => { + const { + attributes: { unique_resource_count }, + } = getScanData(row); + return

{unique_resource_count}

; + }, + }, + + { + id: "actions", + cell: ({ row }) => { + return ; + }, + }, +]; diff --git a/components/scans/table/scans/data-table-row-actions.tsx b/components/scans/table/scans/data-table-row-actions.tsx new file mode 100644 index 0000000000..195851b68c --- /dev/null +++ b/components/scans/table/scans/data-table-row-actions.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { + Button, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownSection, + DropdownTrigger, +} from "@nextui-org/react"; +import { + // DeleteDocumentBulkIcon, + EditDocumentBulkIcon, +} from "@nextui-org/shared-icons"; +import { Row } from "@tanstack/react-table"; +// import clsx from "clsx"; +import { useState } from "react"; + +import { VerticalDotsIcon } from "@/components/icons"; +import { CustomAlertModal } from "@/components/ui/custom"; + +import { EditScanForm } 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 [isEditOpen, setIsEditOpen] = useState(false); + const scanId = (row.original as { id: string }).id; + const scanName = (row.original as any).attributes?.name; + return ( + <> + + + + +
+ + + + + + + } + onClick={() => setIsEditOpen(true)} + > + Edit Scan + + + + +
+ + ); +} diff --git a/components/scans/table/scans/index.ts b/components/scans/table/scans/index.ts new file mode 100644 index 0000000000..d38c1879ca --- /dev/null +++ b/components/scans/table/scans/index.ts @@ -0,0 +1,2 @@ +export * from "./column-get-scans"; +export * from "./data-table-row-actions"; diff --git a/components/scans/table/schedule-scans/column-get-scans-schedule.tsx b/components/scans/table/schedule-scans/column-get-scans-schedule.tsx new file mode 100644 index 0000000000..69bb478ecc --- /dev/null +++ b/components/scans/table/schedule-scans/column-get-scans-schedule.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { PlusIcon } from "@/components/icons"; +import { DateWithTime, EntityInfoShort } from "@/components/ui/entities"; +import { TriggerSheet } from "@/components/ui/sheet"; +import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; +import { ScanProps } from "@/types"; + +import { DataTableRowActions } from "../scans/data-table-row-actions"; +import { DataTableRowDetails } from "."; + +const getScanData = (row: { original: ScanProps }) => { + return row.original; +}; + +export const ColumnGetScansSchedule: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { name }, + } = getScanData(row); + return ; + }, + }, + + { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { state }, + } = getScanData(row); + return ; + }, + }, + { + accessorKey: "scheduled_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { scheduled_at }, + } = getScanData(row); + return ; + }, + }, + + { + id: "moreInfo", + header: "Details", + cell: ({ row }) => { + return ( + } + title="Scan Details" + description="View the scan details" + > + + + ); + }, + }, + + { + id: "actions", + cell: ({ row }) => { + return ; + }, + }, +]; diff --git a/components/scans/table/schedule-scans/data-table-row-details.tsx b/components/scans/table/schedule-scans/data-table-row-details.tsx new file mode 100644 index 0000000000..997fa0e5d9 --- /dev/null +++ b/components/scans/table/schedule-scans/data-table-row-details.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useEffect, useState } from "react"; + +import { getScan } from "@/actions/scans"; +import { ScanDetail, SkeletonTableScans } from "@/components/scans/table"; +import { ScanProps } from "@/types"; + +export const DataTableRowDetails = ({ entityId }: { entityId: string }) => { + const [scanDetails, setScanDetails] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchScanDetails = async () => { + try { + const result = await getScan(entityId); + setScanDetails(result?.data); + } catch (error) { + console.error("Error fetching scan details:", error); + } finally { + setIsLoading(false); + } + }; + + fetchScanDetails(); + }, [entityId]); + + if (isLoading) { + return ; + } + + if (!scanDetails) { + return
No scan details available
; + } + + return ; +}; diff --git a/components/scans/table/schedule-scans/index.ts b/components/scans/table/schedule-scans/index.ts new file mode 100644 index 0000000000..e93f00f4c5 --- /dev/null +++ b/components/scans/table/schedule-scans/index.ts @@ -0,0 +1,2 @@ +export * from "./column-get-scans-schedule"; +export * from "./data-table-row-details"; diff --git a/components/ui/custom/custom-button.tsx b/components/ui/custom/custom-button.tsx index a74751801a..b2a6cf355a 100644 --- a/components/ui/custom/custom-button.tsx +++ b/components/ui/custom/custom-button.tsx @@ -1,6 +1,7 @@ import { Button, CircularProgress } from "@nextui-org/react"; import type { PressEvent } from "@react-types/shared"; import clsx from "clsx"; +import React from "react"; import { NextUIColors, NextUIVariants } from "@/types"; @@ -16,7 +17,7 @@ export const buttonClasses = { hover: "hover:shadow-md", }; -interface ButtonProps { +interface CustomButtonProps { type?: "button" | "submit" | "reset"; ariaLabel: string; ariaDisabled?: boolean; @@ -45,67 +46,80 @@ interface ButtonProps { size?: "sm" | "md" | "lg"; radius?: "none" | "sm" | "md" | "lg" | "full"; dashed?: boolean; - disabled?: boolean; + isDisabled?: boolean; isLoading?: boolean; isIconOnly?: boolean; + ref?: React.RefObject; } -export const CustomButton = ({ - type = "button", - ariaLabel, - ariaDisabled, - className, - variant = "solid", - color = "primary", - onPress, - children, - startContent, - endContent, - size = "md", - radius = "sm", - disabled = false, - isLoading = false, - isIconOnly, - ...props -}: ButtonProps) => ( - + variant = "solid", + color = "primary", + onPress, + children, + startContent, + endContent, + size = "md", + radius = "sm", + isDisabled = false, + isLoading = false, + isIconOnly, + ...props + }, + ref, + ) => ( + + ), ); + +CustomButton.displayName = "CustomButton"; diff --git a/components/providers/date-with-time.tsx b/components/ui/entities/date-with-time.tsx similarity index 86% rename from components/providers/date-with-time.tsx rename to components/ui/entities/date-with-time.tsx index 1e968fae3f..ae89c61985 100644 --- a/components/providers/date-with-time.tsx +++ b/components/ui/entities/date-with-time.tsx @@ -2,7 +2,7 @@ import { format, parseISO } from "date-fns"; import React from "react"; interface DateWithTimeProps { - dateTime: string; // e.g., "2024-07-17T09:55:14.191475Z" + dateTime: string | null; // e.g., "2024-07-17T09:55:14.191475Z" showTime?: boolean; } @@ -10,6 +10,7 @@ export const DateWithTime: React.FC = ({ dateTime, showTime = true, }) => { + if (!dateTime) return --; const date = parseISO(dateTime); const formattedDate = format(date, "MMM dd, yyyy"); const formattedTime = format(date, "p 'UTC'"); diff --git a/components/ui/entities/entity-info-short.tsx b/components/ui/entities/entity-info-short.tsx new file mode 100644 index 0000000000..80a7084223 --- /dev/null +++ b/components/ui/entities/entity-info-short.tsx @@ -0,0 +1,50 @@ +import React from "react"; + +import { + AWSProviderBadge, + AzureProviderBadge, + GCPProviderBadge, + KS8ProviderBadge, +} from "../../icons/providers-badge"; +import { SnippetId } from "./snippet-id"; +import { SnippetLabel } from "./snippet-label"; + +interface EntityInfoProps { + connected?: boolean | null; + cloudProvider?: "aws" | "azure" | "gcp" | "kubernetes"; + entityAlias?: string; + entityId?: string; +} + +export const EntityInfoShort: React.FC = ({ + cloudProvider, + entityAlias, + entityId, +}) => { + const getProviderLogo = () => { + switch (cloudProvider) { + case "aws": + return ; + case "azure": + return ; + case "gcp": + return ; + case "kubernetes": + return ; + default: + return null; + } + }; + + return ( +
+
+
{getProviderLogo()}
+
+ + +
+
+
+ ); +}; diff --git a/components/ui/entities/index.ts b/components/ui/entities/index.ts new file mode 100644 index 0000000000..811b90956c --- /dev/null +++ b/components/ui/entities/index.ts @@ -0,0 +1,5 @@ +export * from "./date-with-time"; +export * from "./entity-info-short"; +export * from "./scan-status"; +export * from "./snippet-id"; +export * from "./snippet-label"; diff --git a/components/providers/scan-status.tsx b/components/ui/entities/scan-status.tsx similarity index 100% rename from components/providers/scan-status.tsx rename to components/ui/entities/scan-status.tsx diff --git a/components/ui/entities/snippet-id.tsx b/components/ui/entities/snippet-id.tsx new file mode 100644 index 0000000000..5d08be7486 --- /dev/null +++ b/components/ui/entities/snippet-id.tsx @@ -0,0 +1,31 @@ +import { Snippet } from "@nextui-org/react"; +import React from "react"; + +import { CopyIcon, DoneIcon, IdIcon } from "@/components/icons"; + +interface SnippetIdProps { + entityId: string; + [key: string]: any; +} +export const SnippetId: React.FC = ({ entityId, ...props }) => { + return ( + } + checkIcon={} + {...props} + > +

+ + + {entityId} + +

+
+ ); +}; diff --git a/components/ui/entities/snippet-label.tsx b/components/ui/entities/snippet-label.tsx new file mode 100644 index 0000000000..18525d6fe5 --- /dev/null +++ b/components/ui/entities/snippet-label.tsx @@ -0,0 +1,32 @@ +import { Snippet } from "@nextui-org/react"; +import React from "react"; + +import { CopyIcon, DoneIcon } from "@/components/icons"; + +interface SnippetLabelProps { + label: string; + [key: string]: any; +} +export const SnippetLabel: React.FC = ({ + label, + ...props +}) => { + return ( + label !== "" && ( + } + checkIcon={} + {...props} + > +

+ {label} +

+
+ ) + ); +}; diff --git a/components/ui/sheet/index.ts b/components/ui/sheet/index.ts new file mode 100644 index 0000000000..2865f1c5a7 --- /dev/null +++ b/components/ui/sheet/index.ts @@ -0,0 +1,2 @@ +export * from "./sheet"; +export * from "./trigger-sheet"; diff --git a/components/ui/sheet/sheet.tsx b/components/ui/sheet/sheet.tsx new file mode 100644 index 0000000000..cf945c0f9d --- /dev/null +++ b/components/ui/sheet/sheet.tsx @@ -0,0 +1,143 @@ +"use client"; + +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-white p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out dark:bg-neutral-950", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right", + }, + }, + defaultVariants: { + side: "right", + }, + }, +); + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + + + Close + + {children} + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = "SheetHeader"; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetFooter.displayName = "SheetFooter"; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetOverlay, + SheetPortal, + SheetTitle, + SheetTrigger, +}; diff --git a/components/ui/sheet/trigger-sheet.tsx b/components/ui/sheet/trigger-sheet.tsx new file mode 100644 index 0000000000..e97e2a45a5 --- /dev/null +++ b/components/ui/sheet/trigger-sheet.tsx @@ -0,0 +1,35 @@ +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "./sheet"; + +interface TriggerSheetProps { + triggerComponent: React.ReactNode; + title: string; + description: string; + children: React.ReactNode; +} + +export function TriggerSheet({ + triggerComponent, + title, + description, + children, +}: TriggerSheetProps) { + return ( + + {triggerComponent} + + + {title} + {description} + + {children} + + + ); +} diff --git a/components/providers/table/data-table-filter-custom.tsx b/components/ui/table/data-table-filter-custom.tsx similarity index 100% rename from components/providers/table/data-table-filter-custom.tsx rename to components/ui/table/data-table-filter-custom.tsx diff --git a/components/ui/table/data-table.tsx b/components/ui/table/data-table.tsx index 3fc9b65dcc..ecec4422a3 100644 --- a/components/ui/table/data-table.tsx +++ b/components/ui/table/data-table.tsx @@ -14,6 +14,7 @@ import { import { useState } from "react"; import { + DataTableFilterCustom, Table, TableBody, TableCell, @@ -22,18 +23,20 @@ import { TableRow, } from "@/components/ui/table"; import { DataTablePagination } from "@/components/ui/table/data-table-pagination"; -import { MetaDataProps } from "@/types"; +import { FilterOption, MetaDataProps } from "@/types"; interface DataTableProviderProps { columns: ColumnDef[]; data: TData[]; metadata?: MetaDataProps; + customFilters?: FilterOption[]; } export function DataTable({ columns, data, metadata, + customFilters, }: DataTableProviderProps) { const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); @@ -55,6 +58,11 @@ export function DataTable({ return ( <> + {customFilters && ( +
+ +
+ )}
diff --git a/components/ui/table/index.ts b/components/ui/table/index.ts index 67cf0fab8b..a344b959de 100644 --- a/components/ui/table/index.ts +++ b/components/ui/table/index.ts @@ -1,5 +1,6 @@ export * from "./data-table"; export * from "./data-table-column-header"; +export * from "./data-table-filter-custom"; export * from "./data-table-pagination"; export * from "./severity-badge"; export * from "./status-badge"; diff --git a/components/ui/table/status-badge.tsx b/components/ui/table/status-badge.tsx index e688021cd4..c0f0356407 100644 --- a/components/ui/table/status-badge.tsx +++ b/components/ui/table/status-badge.tsx @@ -2,38 +2,42 @@ import { Chip } from "@nextui-org/react"; import React from "react"; type Status = + | "available" + | "scheduled" + | "executing" | "completed" - | "pending" - | "cancelled" - | "fail" - | "success" - | "muted" - | "active" - | "inactive"; + | "failed" + | "cancelled"; const statusColorMap: Record< Status, "danger" | "warning" | "success" | "default" > = { + available: "default", + scheduled: "warning", + executing: "default", completed: "success", - pending: "warning", + failed: "danger", cancelled: "danger", - fail: "danger", - success: "success", - muted: "default", - active: "success", - inactive: "default", }; -export const StatusBadge = ({ status }: { status: Status }) => { +export const StatusBadge = ({ + status, + size = "sm", + ...props +}: { + status: Status; + size?: "sm" | "md" | "lg"; +}) => { const color = statusColorMap[status as keyof typeof statusColorMap]; return ( {status} diff --git a/components/users/table/ColumnsUser.tsx b/components/users/table/ColumnsUser.tsx index 605120c707..20ac2dba9d 100644 --- a/components/users/table/ColumnsUser.tsx +++ b/components/users/table/ColumnsUser.tsx @@ -2,8 +2,7 @@ import { ColumnDef } from "@tanstack/react-table"; -import { DateWithTime } from "@/components/providers"; -import { StatusBadge } from "@/components/ui/table"; +import { DateWithTime } from "@/components/ui/entities"; import { UserActions } from "@/components/users"; import { UserProps } from "@/types"; @@ -44,14 +43,7 @@ export const ColumnsUser: ColumnDef[] = [ return ; }, }, - { - accessorKey: "status", - header: "Status", - cell: ({ row }) => { - const { status } = getUserData(row); - return ; - }, - }, + { accessorKey: "actions", header: () =>
Actions
, 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/package-lock.json b/package-lock.json index 1f8510886b..f060d1495a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@nextui-org/system": "2.2.1", "@nextui-org/theme": "2.2.5", "@radix-ui/react-alert-dialog": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", @@ -44,7 +44,7 @@ "recharts": "^2.13.0-alpha.4", "server-only": "^0.0.1", "shadcn-ui": "^0.2.3", - "tailwind-merge": "^2.5.2", + "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", "uuid": "^10.0.0", "zod": "^3.23.8", @@ -2748,6 +2748,67 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", @@ -2824,24 +2885,130 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", - "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.0", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" + "react-remove-scroll": "2.6.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -2859,11 +3026,12 @@ } }, "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "license": "MIT", "dependencies": { - "react-remove-scroll-bar": "^2.3.4", + "react-remove-scroll-bar": "^2.3.6", "react-style-singleton": "^2.2.1", "tslib": "^2.1.0", "use-callback-ref": "^1.3.0", @@ -12211,9 +12379,9 @@ } }, "node_modules/tailwind-merge": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz", - "integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.3.tgz", + "integrity": "sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==", "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index 67ac210f66..2d9f0f0ba4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@nextui-org/system": "2.2.1", "@nextui-org/theme": "2.2.5", "@radix-ui/react-alert-dialog": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", @@ -36,7 +36,7 @@ "recharts": "^2.13.0-alpha.4", "server-only": "^0.0.1", "shadcn-ui": "^0.2.3", - "tailwind-merge": "^2.5.2", + "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", "uuid": "^10.0.0", "zod": "^3.23.8", diff --git a/types/components.ts b/types/components.ts index 02018469dc..f88967e85e 100644 --- a/types/components.ts +++ b/types/components.ts @@ -66,6 +66,47 @@ export interface ProviderProps { }; } +export interface ScanProps { + type: "Scan"; + id: string; + attributes: { + name: string; + trigger: "scheduled" | "manual"; + state: + | "available" + | "scheduled" + | "executing" + | "completed" + | "failed" + | "cancelled"; + unique_resource_count: number; + progress: number; + scanner_args: { + only_logs?: boolean; + excluded_checks?: string[]; + aws_retries_max_attempts?: number; + } | null; + duration: number; + started_at: string; + completed_at: string; + scheduled_at: string; + }; + relationships: { + provider: { + data: { + id: string; + type: "Provider"; + }; + }; + task: { + data: { + id: string; + type: "Task"; + }; + }; + }; +} + export interface FindingProps { id: string; attributes: { diff --git a/types/formSchemas.ts b/types/formSchemas.ts index 3726245f68..e1df1285b8 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -1,5 +1,36 @@ import { z } from "zod"; +export const editScanFormSchema = (currentName: string) => + z.object({ + scanName: z + .string() + .refine((val) => val === "" || val.length >= 3, { + message: "The alias must be empty or have at least 3 characters.", + }) + .refine((val) => val !== currentName, { + message: "The new name must be different from the current one.", + }) + .optional(), + scanId: z.string(), + }); + +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(),