diff --git a/actions/providers.ts b/actions/providers.ts index d4b0dc4d29..a35bbb1d98 100644 --- a/actions/providers.ts +++ b/actions/providers.ts @@ -6,6 +6,10 @@ import { redirect } from "next/navigation"; import { auth } from "@/auth.config"; import { parseStringify } from "@/lib"; +// Credentials for basic auth +const username = process.env.API_USERNAME; +const password = process.env.API_PASSWORD; + export const getProviders = async ({ page = 1, query = "", @@ -18,7 +22,7 @@ export const getProviders = async ({ if (isNaN(Number(page)) || page < 1) redirect("/providers"); const keyServer = process.env.API_BASE_URL; - const url = new URL(`${keyServer}/providers`); + const url = new URL(`${keyServer}/providers?sort=-inserted_at`); if (page) url.searchParams.append("page[number]", page.toString()); if (query) url.searchParams.append("filter[search]", query); @@ -35,6 +39,7 @@ export const getProviders = async ({ const providers = await fetch(url.toString(), { headers: { "X-Tenant-ID": `${tenantId}`, + Authorization: "Basic " + btoa(`${username}:${password}`), }, }); const data = await providers.json(); @@ -59,6 +64,7 @@ export const getProvider = async (formData: FormData) => { const providers = await fetch(url.toString(), { headers: { "X-Tenant-ID": `${tenantId}`, + Authorization: "Basic " + btoa(`${username}:${password}`), }, }); const data = await providers.json(); @@ -86,6 +92,7 @@ export const updateProvider = async (formData: FormData) => { method: "PATCH", headers: { "X-Tenant-ID": `${tenantId}`, + Authorization: "Basic " + btoa(`${username}:${password}`), "Content-Type": "application/vnd.api+json", }, body: JSON.stringify({ @@ -110,26 +117,32 @@ export const updateProvider = async (formData: FormData) => { }; export const addProvider = async (formData: FormData) => { + const session = await auth(); const keyServer = process.env.API_BASE_URL; - const provider = formData.get("provider"); - const providerId = formData.get("id"); - const alias = formData.get("alias"); + const tenantId = session?.user.tenantId; + + const providerType = formData.get("providerType"); + const providerId = formData.get("providerId"); + const providerAlias = formData.get("providerAlias"); + + const url = new URL(`${keyServer}/providers`); try { - const response = await fetch(`${keyServer}/providers`, { + const response = await fetch(url.toString(), { method: "POST", headers: { - "X-Tenant-ID": `${process.env.HEADER_TENANT_ID}`, + "X-Tenant-ID": `${tenantId}`, + Authorization: "Basic " + btoa(`${username}:${password}`), "Content-Type": "application/vnd.api+json", }, body: JSON.stringify({ data: { type: "Provider", attributes: { - provider: provider, - provider_id: providerId, - alias: alias, + provider: providerType, + uid: providerId, + alias: providerAlias, }, }, }), @@ -146,40 +159,20 @@ export const addProvider = async (formData: FormData) => { }; export const checkConnectionProvider = async (formData: FormData) => { + const session = await auth(); const keyServer = process.env.API_BASE_URL; + const tenantId = session?.user.tenantId; const providerId = formData.get("id"); - try { - const response = await fetch( - `${keyServer}/providers/${providerId}/connection`, - { - method: "POST", - headers: { - "X-Tenant-ID": `${process.env.HEADER_TENANT_ID}`, - }, - }, - ); - const data = await response.json(); - revalidatePath("/providers"); - return parseStringify(data); - } catch (error) { - return { - error: getErrorMessage(error), - }; - } -}; - -export const deleteProvider = async (formData: FormData) => { - const keyServer = process.env.API_BASE_URL; - - const providerId = formData.get("id"); + const url = new URL(`${keyServer}/providers/${providerId}/connection`); try { - const response = await fetch(`${keyServer}/providers/${providerId}`, { - method: "DELETE", + const response = await fetch(url.toString(), { + method: "POST", headers: { - "X-Tenant-ID": `${process.env.HEADER_TENANT_ID}`, + "X-Tenant-ID": `${tenantId}`, + Authorization: "Basic " + btoa(`${username}:${password}`), }, }); const data = await response.json(); @@ -191,6 +184,33 @@ export const deleteProvider = async (formData: FormData) => { }; } }; + +export const deleteProvider = async (formData: FormData) => { + const session = await auth(); + const keyServer = process.env.API_BASE_URL; + const tenantId = session?.user.tenantId; + + const providerId = formData.get("id"); + const url = new URL(`${keyServer}/providers/${providerId}`); + + try { + const response = await fetch(url.toString(), { + method: "DELETE", + headers: { + "X-Tenant-ID": `${tenantId}`, + Authorization: "Basic " + btoa(`${username}:${password}`), + }, + }); + const data = await response.json(); + revalidatePath("/providers"); + return parseStringify(data); + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; + export const getErrorMessage = (error: unknown): string => { let message: string; diff --git a/app/(prowler)/layout.tsx b/app/(prowler)/layout.tsx index b30817c0ba..ebf66c7975 100644 --- a/app/(prowler)/layout.tsx +++ b/app/(prowler)/layout.tsx @@ -46,7 +46,7 @@ export default function RootLayout({
-
+
{children}
diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index 6a79a8a872..a10cd9a618 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -2,8 +2,8 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getProviders } from "@/actions"; -import { FilterControls } from "@/components/filters"; -import { AddProviderModal } from "@/components/providers"; +import { FilterControls, filtersProviders } from "@/components/filters"; +import { AddProvider } from "@/components/providers"; import { ColumnsProvider, DataTableProvider, @@ -22,18 +22,17 @@ export default async function Providers({ return ( <>
+ - + -
-
- -
- - }> - - -
+ + + + + }> + + ); } diff --git a/components/filters/FilterControls.tsx b/components/filters/FilterControls.tsx deleted file mode 100644 index d55386ddc9..0000000000 --- a/components/filters/FilterControls.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { Button } from "@nextui-org/react"; -import { useRouter, useSearchParams } from "next/navigation"; -import React, { useCallback, useEffect, useState } from "react"; - -import { CustomAccountSelection } from "./CustomAccountSelection"; -import { CustomCheckboxMutedFindings } from "./CustomCheckboxMutedFindings"; -import { CustomDatePicker } from "./CustomDatePicker"; -import { CustomRegionSelection } from "./CustomRegionSelection"; -import { CustomSearchInput } from "./CustomSearchInput"; -import { CustomSelectProvider } from "./CustomSelectProvider"; - -interface FilterControlsProps { - search?: boolean; - providers?: boolean; - date?: boolean; - regions?: boolean; - accounts?: boolean; - mutedFindings?: boolean; -} - -export const FilterControls: React.FC = ({ - search = false, - providers = false, - date = false, - regions = false, - accounts = false, - mutedFindings = false, -}) => { - const router = useRouter(); - const searchParams = useSearchParams(); - const [showClearButton, setShowClearButton] = useState(false); - - useEffect(() => { - const hasFilters = Array.from(searchParams.keys()).some( - (key) => key.startsWith("filter[") || key === "sort", - ); - setShowClearButton(hasFilters); - }, [searchParams]); - - const clearAllFilters = useCallback(() => { - const params = new URLSearchParams(searchParams.toString()); - Array.from(params.keys()).forEach((key) => { - if (key.startsWith("filter[") || key === "sort") { - params.delete(key); - } - }); - router.push(`?${params.toString()}`, { scroll: false }); - }, [router, searchParams]); - - return ( -
- {search && } - {providers && } - {date && } - {regions && } - {accounts && } - {mutedFindings && } - - {showClearButton && ( - - )} -
- ); -}; diff --git a/components/filters/CustomCheckboxMutedFindings.tsx b/components/filters/custo-checkbox-muted-findings.tsx similarity index 100% rename from components/filters/CustomCheckboxMutedFindings.tsx rename to components/filters/custo-checkbox-muted-findings.tsx diff --git a/components/filters/CustomAccountSelection.tsx b/components/filters/custom-account-selection.tsx similarity index 100% rename from components/filters/CustomAccountSelection.tsx rename to components/filters/custom-account-selection.tsx diff --git a/components/filters/CustomDatePicker.tsx b/components/filters/custom-date-picker.tsx similarity index 92% rename from components/filters/CustomDatePicker.tsx rename to components/filters/custom-date-picker.tsx index 820407d6c2..615acc5dd4 100644 --- a/components/filters/CustomDatePicker.tsx +++ b/components/filters/custom-date-picker.tsx @@ -28,9 +28,9 @@ export const CustomDatePicker = () => { (date: any) => { const params = new URLSearchParams(searchParams.toString()); if (date) { - params.set("filter[updated_at__lte]", date.toString()); + params.set("filter[updated_at]", date.toString()); } else { - params.delete("filter[updated_at__lte]"); + params.delete("filter[updated_at]"); } router.push(`?${params.toString()}`, { scroll: false }); }, @@ -59,7 +59,6 @@ export const CustomDatePicker = () => { return (
} CalendarTopContent={ { }} value={value} onChange={handleDateChange} - label="Scan date" size="sm" variant="flat" /> diff --git a/components/filters/CustomProviderInputs.tsx b/components/filters/custom-provider-inputs.tsx similarity index 100% rename from components/filters/CustomProviderInputs.tsx rename to components/filters/custom-provider-inputs.tsx diff --git a/components/filters/CustomRegionSelection.tsx b/components/filters/custom-region-selection.tsx similarity index 100% rename from components/filters/CustomRegionSelection.tsx rename to components/filters/custom-region-selection.tsx diff --git a/components/filters/CustomSearchInput.tsx b/components/filters/custom-search-input.tsx similarity index 88% rename from components/filters/CustomSearchInput.tsx rename to components/filters/custom-search-input.tsx index 98310f10b5..f958c9d6bd 100644 --- a/components/filters/CustomSearchInput.tsx +++ b/components/filters/custom-search-input.tsx @@ -1,6 +1,6 @@ import { Input } from "@nextui-org/react"; import debounce from "lodash.debounce"; -import { XCircle } from "lucide-react"; +import { SearchIcon, XCircle } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import React, { useCallback, useEffect, useState } from "react"; @@ -37,11 +37,11 @@ export const CustomSearchInput: React.FC = () => { return ( } onChange={(e) => { const value = e.target.value; setSearchQuery(value); @@ -54,6 +54,7 @@ export const CustomSearchInput: React.FC = () => { ) } + radius="sm" size="sm" /> ); diff --git a/components/filters/CustomSelectProvider.tsx b/components/filters/custom-select-provider.tsx similarity index 86% rename from components/filters/CustomSelectProvider.tsx rename to components/filters/custom-select-provider.tsx index dd4254fdd3..ef65220660 100644 --- a/components/filters/CustomSelectProvider.tsx +++ b/components/filters/custom-select-provider.tsx @@ -9,7 +9,7 @@ import { CustomProviderInputAzure, CustomProviderInputGCP, CustomProviderInputKubernetes, -} from "./CustomProviderInputs"; +} from "./custom-provider-inputs"; const dataInputsProvider = [ { @@ -43,9 +43,9 @@ export const CustomSelectProvider = () => { (value: string) => { const params = new URLSearchParams(searchParams.toString()); if (value) { - params.set("filter[provider]", value); + params.set("filter[provider__in]", value); } else { - params.delete("filter[provider]"); + params.delete("filter[provider__in]"); } router.push(`?${params.toString()}`, { scroll: false }); }, @@ -53,21 +53,18 @@ export const CustomSelectProvider = () => { ); useEffect(() => { - const providerFromUrl = searchParams.get("filter[provider]") || ""; + const providerFromUrl = searchParams.get("filter[provider__in]") || ""; setSelectedProvider(providerFromUrl); }, [searchParams]); return ( - - - - - ); -}; diff --git a/components/providers/AddProviderModal.tsx b/components/providers/AddProviderModal.tsx deleted file mode 100644 index c81602198b..0000000000 --- a/components/providers/AddProviderModal.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client"; - -import { Button, Input } from "@nextui-org/react"; -import { useRef, useState } from "react"; - -import { addProvider } from "@/actions"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, - useToast, -} from "@/components/ui"; - -import { ButtonAddProvider } from "./ButtonAddProvider"; -import { CustomRadioProvider } from "./CustomRadioProvider"; - -export const AddProviderModal = () => { - const [open, setOpen] = useState(false); - - const ref = useRef(null); - const { toast } = useToast(); - - async function clientAction(formData: FormData) { - // reset the form - ref.current?.reset(); - // client-side validation - const data = await addProvider(formData); - if (data?.errors) { - data.errors.forEach((error: { detail: string }) => { - const errorMessage = `${error.detail}`; - // show error - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - }); - } else { - toast({ - title: "Success!", - description: "The provider was added successfully.", - }); - } - } - return ( - - - - - - - - Add cloud account - - - You must manually deploy a new read-only IAM role for each account - you want to add. The following links will provide detailed - instructions how to do this: - - -
setOpen(false)} - className="grid sm:grid-cols-2 gap-6" - > -
- -
-
- - -
-
- -
-
-
-
- ); -}; diff --git a/components/providers/ButtonAddProvider.tsx b/components/providers/ButtonAddProvider.tsx deleted file mode 100644 index 30f537ceae..0000000000 --- a/components/providers/ButtonAddProvider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -"use client"; - -import { Button, CircularProgress } from "@nextui-org/react"; -import React from "react"; -import { useFormStatus } from "react-dom"; - -export const ButtonAddProvider = () => { - const { pending } = useFormStatus(); - return ( - - ); -}; diff --git a/components/providers/CustomRadioProvider.tsx b/components/providers/CustomRadioProvider.tsx index 05c9926b0a..68d2c59576 100644 --- a/components/providers/CustomRadioProvider.tsx +++ b/components/providers/CustomRadioProvider.tsx @@ -3,9 +3,15 @@ import { UseRadioProps } from "@nextui-org/radio/dist/use-radio"; import { cn, RadioGroup, useRadio, VisuallyHidden } from "@nextui-org/react"; import React from "react"; +import { Control, Controller } from "react-hook-form"; +import { z } from "zod"; + +import { addProviderFormSchema } from "@/types"; import { AWSProviderBadge, AzureProviderBadge } from "../icons/providers-badge"; import { GCPProviderBadge } from "../icons/providers-badge/GCPProviderBadge"; +import { KS8ProviderBadge } from "../icons/providers-badge/KS8ProviderBadge"; +import { FormMessage } from "../ui/form"; interface CustomRadioProps extends UseRadioProps { description?: string; @@ -52,27 +58,48 @@ export const CustomRadio: React.FC = (props) => { ); }; -export const CustomRadioProvider = () => { +interface CustomRadioProviderProps { + control: Control>; +} + +export const CustomRadioProvider: React.FC = ({ + control, +}) => { return ( - - -
- - AWS -
-
- -
- - GCP -
-
- -
- - Azure -
-
-
+ ( + <> + + +
+ + AWS +
+
+ +
+ + GCP +
+
+ +
+ + Azure +
+
+ +
+ + Kubernetes +
+
+
+ + + )} + /> ); }; diff --git a/components/providers/SnippetIdProvider.tsx b/components/providers/SnippetIdProvider.tsx index 775e72eaf7..4d4b901f2c 100644 --- a/components/providers/SnippetIdProvider.tsx +++ b/components/providers/SnippetIdProvider.tsx @@ -12,6 +12,7 @@ export const SnippetIdProvider: React.FC = ({ return ( = ({ >

- {providerId} + + {providerId} +

); diff --git a/components/providers/add-provider.tsx b/components/providers/add-provider.tsx new file mode 100644 index 0000000000..b884fa5292 --- /dev/null +++ b/components/providers/add-provider.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useState } from "react"; + +import { AddIcon } from "../icons"; +import { CustomAlertModal, CustomButton } from "../ui/custom"; +import { AddForm } from "./forms"; + +export const AddProvider = () => { + const [isAddOpen, setIsAddOpen] = useState(false); + + return ( + <> + + + + +
+ setIsAddOpen(true)} + endContent={} + > + Add Account + +
+ + ); +}; diff --git a/components/providers/forms/AddForm.tsx b/components/providers/forms/AddForm.tsx new file mode 100644 index 0000000000..be508dca0b --- /dev/null +++ b/components/providers/forms/AddForm.tsx @@ -0,0 +1,122 @@ +"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 { addProvider } from "@/actions"; +import { SaveIcon } from "@/components/icons"; +import { useToast } from "@/components/ui"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; +import { addProviderFormSchema } from "@/types"; + +import { CustomRadioProvider } from "../CustomRadioProvider"; + +export const AddForm = ({ + setIsOpen, +}: { + setIsOpen: Dispatch>; +}) => { + const formSchema = addProviderFormSchema; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + providerType: "", + providerId: "", + providerAlias: "", + }, + }); + + 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 addProvider(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 provider was updated successfully.", + }); + setIsOpen(false); // Close the modal on success + } + }; + + return ( +
+ + + + + +
+ setIsOpen(false)} + disabled={isLoading} + > + Cancel + + + } + > + {isLoading ? <>Loading : Confirm} + +
+ + + ); +}; diff --git a/components/providers/forms/DeleteForm.tsx b/components/providers/forms/DeleteForm.tsx index 2a9d79902c..ec478fe690 100644 --- a/components/providers/forms/DeleteForm.tsx +++ b/components/providers/forms/DeleteForm.tsx @@ -1,13 +1,14 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, CircularProgress } from "@nextui-org/react"; import React, { Dispatch, SetStateAction } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { deleteProvider } from "@/actions"; +import { DeleteIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; +import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; const formSchema = z.object({ @@ -46,6 +47,7 @@ export const DeleteForm = ({ description: "The provider was removed successfully.", }); } + setIsOpen(false); // Close the modal on success } return ( @@ -53,31 +55,29 @@ export const DeleteForm = ({
- - + Cancel + + + } + > + {isLoading ? <>Loading : Delete} +
diff --git a/components/providers/forms/EditForm.tsx b/components/providers/forms/EditForm.tsx index 8479aeeabb..c8581caccc 100644 --- a/components/providers/forms/EditForm.tsx +++ b/components/providers/forms/EditForm.tsx @@ -1,14 +1,14 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, CircularProgress } from "@nextui-org/react"; -import React, { Dispatch, SetStateAction } from "react"; +import { Dispatch, SetStateAction } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { updateProvider } from "@/actions"; +import { SaveIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; -import { CustomInput } from "@/components/ui/custom"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { editProviderFormSchema } from "@/types"; @@ -32,6 +32,7 @@ export const EditForm = ({ }); const { toast } = useToast(); + const isLoading = form.formState.isSubmitting; const onSubmitClient = async (values: z.infer) => { @@ -86,29 +87,29 @@ export const EditForm = ({
- - - + Cancel + + + } + > + {isLoading ? <>Loading : Save} +
diff --git a/components/providers/forms/index.ts b/components/providers/forms/index.ts index af1cdc1b37..7bab77754f 100644 --- a/components/providers/forms/index.ts +++ b/components/providers/forms/index.ts @@ -1,2 +1,3 @@ +export * from "./AddForm"; export * from "./DeleteForm"; export * from "./EditForm"; diff --git a/components/providers/index.ts b/components/providers/index.ts index b92fe7eb83..0709c892b2 100644 --- a/components/providers/index.ts +++ b/components/providers/index.ts @@ -1,6 +1,4 @@ -export * from "./AddProvider"; -export * from "./AddProviderModal"; -export * from "./ButtonAddProvider"; +export * from "./add-provider"; export * from "./CheckConnectionProvider"; export * from "./CustomRadioProvider"; export * from "./DateWithTime"; diff --git a/components/providers/table/columns-provider.tsx b/components/providers/table/columns-provider.tsx index 2b7ecbb5a5..5f19489948 100644 --- a/components/providers/table/columns-provider.tsx +++ b/components/providers/table/columns-provider.tsx @@ -3,7 +3,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { add } from "date-fns"; -import { StatusBadge } from "@/components/ui"; +import { StatusBadge } from "@/components/ui/table"; import { ProviderProps } from "@/types"; import { DateWithTime } from "../DateWithTime"; @@ -40,15 +40,15 @@ export const ColumnsProvider: ColumnDef[] = [ }, }, { - accessorKey: "id", + accessorKey: "uid", header: ({ column }) => ( ), cell: ({ row }) => { const { - attributes: { provider_id }, + attributes: { uid }, } = getProviderData(row); - return ; + return ; }, }, { diff --git a/components/providers/table/data-table-column-header.tsx b/components/providers/table/data-table-column-header.tsx index 3b460ae937..4df6af2d78 100644 --- a/components/providers/table/data-table-column-header.tsx +++ b/components/providers/table/data-table-column-header.tsx @@ -2,13 +2,14 @@ import { Button } from "@nextui-org/react"; import { Column } from "@tanstack/react-table"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { HTMLAttributes } from "react"; + import { ArrowDownIcon, ArrowUpIcon, ChevronsLeftRightIcon, -} from "lucide-react"; -import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { HTMLAttributes } from "react"; +} from "@/components/icons"; interface DataTableColumnHeaderProps extends HTMLAttributes { @@ -64,12 +65,12 @@ export const DataTableColumnHeader = ({ currentSortParam === "" || (currentSortParam !== param && currentSortParam !== `-${param}`) ) { - return ; + return ; } return currentSortParam === `-${param}` ? ( - + ) : ( - + ); }; @@ -79,9 +80,7 @@ export const DataTableColumnHeader = ({ return ( - )), - )} +
+ } + onPress={() => setShowFilters(!showFilters)} + > + {showFilters ? "Hide Filters" : "Show Filters"} + + +
+
+ {filters.map((filter) => ( + + ))} +
+ + Selected +
+
+
); -} +}; diff --git a/components/providers/table/data-table-pagination.tsx b/components/providers/table/data-table-pagination.tsx index 0e195f0507..4c4278682f 100644 --- a/components/providers/table/data-table-pagination.tsx +++ b/components/providers/table/data-table-pagination.tsx @@ -9,7 +9,7 @@ import { import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; -import { extractPaginationInfo } from "@/lib"; +import { getPaginationInfo } from "@/lib"; import { MetaDataProps } from "@/types"; interface DataTablePaginationProps { @@ -22,8 +22,7 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) { const pathname = usePathname(); const searchParams = useSearchParams(); - const { currentPage, totalPages, totalEntries } = - extractPaginationInfo(metadata); + const { currentPage, totalPages, totalEntries } = getPaginationInfo(metadata); const createPageUrl = (pageNumber: number | string) => { const params = new URLSearchParams(searchParams); diff --git a/components/providers/table/data-table-provider.tsx b/components/providers/table/data-table-provider.tsx index e300496913..b2a0655779 100644 --- a/components/providers/table/data-table-provider.tsx +++ b/components/providers/table/data-table-provider.tsx @@ -20,10 +20,9 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui"; +} from "@/components/ui/table"; import { MetaDataProps } from "@/types"; -import { DataTableFilterCustom } from "./data-table-filter-custom"; import { DataTablePagination } from "./data-table-pagination"; interface DataTableProviderProps { @@ -55,20 +54,9 @@ export function DataTableProvider({ }, }); - // This will be used to have "custom" filters in the table and will be - // passed to the DataTableFilterCustom component. This needs to be dynamic - // based on the columns that are available in the table. - - const filters = [ - { key: "provider", values: ["aws", "gcp", "azure"] }, - { key: "connected", values: ["false", "true"] }, - // Add more filter categories as needed - ]; - return ( <> - -
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/components/ui/custom/CustomButton.tsx b/components/ui/custom/CustomButton.tsx new file mode 100644 index 0000000000..9aa3b4a888 --- /dev/null +++ b/components/ui/custom/CustomButton.tsx @@ -0,0 +1,96 @@ +import { Button, CircularProgress } from "@nextui-org/react"; +import type { PressEvent } from "@react-types/shared"; +import clsx from "clsx"; + +import { NextUIColors, NextUIVariants } from "@/types"; + +export const buttonClasses = { + base: "px-2 inline-flex items-center justify-center relative z-0 text-center whitespace-nowrap", + primary: "bg-default-100 hover:bg-default-200 text-default-800", + secondary: "bg-prowler-grey-light dark:bg-prowler-grey-medium text-white", + action: "text-white bg-prowler-blue-smoky dark:bg-prowler-grey-medium", + dashed: + "border border-default border-dashed bg-transparent justify-center whitespace-nowrap font-medium shadow-sm hover:border-solid hover:bg-default-100 active:bg-default-200 active:border-solid", + transparent: "border-0 border-transparent bg-transparent", + disabled: "pointer-events-none opacity-40", + hover: "hover:shadow-md", +}; + +interface ButtonProps { + type?: "button" | "submit" | "reset"; + className?: string; + variant?: + | "solid" + | "faded" + | "bordered" + | "light" + | "flat" + | "ghost" + | "dashed" + | "shadow"; + color?: + | "primary" + | "secondary" + | "action" + | "success" + | "warning" + | "danger" + | "transparent"; + onPress?: (e: PressEvent) => void; + children: React.ReactNode; + startContent?: React.ReactNode; + endContent?: React.ReactNode; + size?: "sm" | "md" | "lg"; + radius?: "none" | "sm" | "md" | "lg" | "full"; + dashed?: boolean; + disabled?: boolean; + isLoading?: boolean; + isIconOnly?: boolean; +} + +export const CustomButton = ({ + type = "button", + className, + variant = "solid", + color = "primary", + onPress, + children, + startContent, + endContent, + size = "md", + radius = "sm", + disabled = false, + isLoading = false, + isIconOnly, + ...props +}: ButtonProps) => ( + +); diff --git a/components/ui/custom/custom-dropdown-filter.tsx b/components/ui/custom/custom-dropdown-filter.tsx new file mode 100644 index 0000000000..f8d9d82b96 --- /dev/null +++ b/components/ui/custom/custom-dropdown-filter.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { + Badge, + Button, + Checkbox, + CheckboxGroup, + Divider, + Popover, + PopoverContent, + PopoverTrigger, +} from "@nextui-org/react"; +import { useSearchParams } from "next/navigation"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; + +import { PlusCircleIcon } from "@/components/icons"; +import { CustomDropdownFilterProps } from "@/types"; + +export const CustomDropdownFilter: React.FC = ({ + filter, + onFilterChange, +}) => { + const searchParams = useSearchParams(); + const [groupSelected, setGroupSelected] = useState(new Set()); + + const allFilterKeys = filter?.values || []; + + const getActiveFilter = useMemo(() => { + const currentFilters: Record = {}; + Array.from(searchParams.entries()).forEach(([key, value]) => { + if (key.startsWith("filter[") && key.endsWith("]")) { + const filterKey = key.slice(7, -1); + if (filter && filter.key === filterKey) { + // eslint-disable-next-line security/detect-object-injection + currentFilters[filterKey] = value; + } + } + }); + return currentFilters; + }, [searchParams, filter]); + + useEffect(() => { + if (filter && getActiveFilter[filter.key]) { + const activeValues = getActiveFilter[filter.key].split(","); + const newSelection = new Set(activeValues); + if (newSelection.size === allFilterKeys.length) { + newSelection.add("all"); + } + setGroupSelected(newSelection); + } else { + setGroupSelected(new Set()); + } + }, [getActiveFilter, filter, allFilterKeys]); + + const onSelectionChange = useCallback( + (keys: string[]) => { + const newSelection = new Set(keys); + + if ( + newSelection.size === allFilterKeys.length && + !newSelection.has("all") + ) { + setGroupSelected(new Set(["all", ...allFilterKeys])); + } else if (groupSelected.has("all")) { + newSelection.delete("all"); + const remainingValues = allFilterKeys.filter((key) => + newSelection.has(key), + ); + setGroupSelected(new Set(remainingValues)); + } else { + setGroupSelected(newSelection); + } + + if (onFilterChange && filter) { + const selectedValues = Array.from(newSelection).filter( + (key) => key !== "all", + ); + onFilterChange(filter.key, selectedValues); + } + }, + [allFilterKeys, groupSelected, onFilterChange, filter], + ); + + const handleSelectAllClick = useCallback(() => { + if (groupSelected.has("all")) { + setGroupSelected(new Set()); + } else { + setGroupSelected(new Set(["all", ...allFilterKeys])); + } + }, [groupSelected, allFilterKeys]); + return ( +
+ + + + + +
+ + + Select All + + {allFilterKeys.map((value) => ( + + {value} + + ))} + +
+
+
+ {groupSelected?.size > 0 && ( +

+ Selected:{" "} + {Array.from(groupSelected) + .filter((item) => item !== "all") + .join(", ")} +

+ )} +
+ ); +}; diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts index 0520404750..dc543cb6ca 100644 --- a/components/ui/custom/index.ts +++ b/components/ui/custom/index.ts @@ -1,5 +1,7 @@ +export * from "./custom-dropdown-filter"; export * from "./CustomAlertModal"; export * from "./CustomBox"; +export * from "./CustomButton"; export * from "./CustomButtonClientAction"; export * from "./CustomInput"; export * from "./CustomLoader"; diff --git a/components/ui/dialog/Dialog.tsx b/components/ui/dialog/dialog.tsx similarity index 76% rename from components/ui/dialog/Dialog.tsx rename to components/ui/dialog/dialog.tsx index a25a3caebc..93542df32f 100644 --- a/components/ui/dialog/Dialog.tsx +++ b/components/ui/dialog/dialog.tsx @@ -1,7 +1,7 @@ "use client"; import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { X } from "lucide-react"; +import { Cross2Icon } from "@radix-ui/react-icons"; import * as React from "react"; import { cn } from "@/lib/utils"; @@ -38,14 +38,14 @@ const DialogContent = React.forwardRef< {children} - - + + Close @@ -102,7 +102,7 @@ const DialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/components/ui/index.ts b/components/ui/index.ts index 17f6f0e874..07631d9636 100644 --- a/components/ui/index.ts +++ b/components/ui/index.ts @@ -2,13 +2,10 @@ export * from "./action-card/ActionCard"; export * from "./alert/Alert"; export * from "./alert-dialog/AlertDialog"; export * from "./chart/Chart"; -export * from "./dialog/Dialog"; +export * from "./dialog/dialog"; export * from "./dropdown/Dropdown"; export * from "./header/Header"; export * from "./label/Label"; export * from "./select/Select"; export * from "./sidebar"; -export * from "./table/SeverityBadge"; -export * from "./table/StatusBadge"; -export * from "./table/Table"; export * from "./toast"; diff --git a/components/ui/sidebar/SidebarWrap.tsx b/components/ui/sidebar/SidebarWrap.tsx index bb9287a826..2da8d70670 100644 --- a/components/ui/sidebar/SidebarWrap.tsx +++ b/components/ui/sidebar/SidebarWrap.tsx @@ -42,7 +42,7 @@ export const SidebarWrap = () => { return (
{ />{" "} - + >(({ className, ...props }, ref) => ( -
+
>(({ className, ...props }, ref) => ( - + tr]:first:rounded-lg [&>tr]:first:shadow-small", + className, + )} + {...props} + /> )); TableHeader.displayName = "TableHeader"; @@ -28,11 +34,7 @@ const TableBody = React.forwardRef< HTMLTableSectionElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( - + )); TableBody.displayName = "TableBody"; @@ -43,7 +45,7 @@ const TableFooter = React.forwardRef< tr]:last:border-b-0 dark:bg-slate-800/50", + "font-medium [&>tr]:last:border-b-0 dark:bg-slate-800/50", className, )} {...props} @@ -58,7 +60,7 @@ const TableRow = React.forwardRef< (({ className, ...props }, ref) => (
)); @@ -99,7 +104,7 @@ const TableCaption = React.forwardRef< >(({ className, ...props }, ref) => (
)); diff --git a/components/users/table/ColumnsUser.tsx b/components/users/table/ColumnsUser.tsx index 70a05c189a..605120c707 100644 --- a/components/users/table/ColumnsUser.tsx +++ b/components/users/table/ColumnsUser.tsx @@ -3,7 +3,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { DateWithTime } from "@/components/providers"; -import { StatusBadge } from "@/components/ui"; +import { StatusBadge } from "@/components/ui/table"; import { UserActions } from "@/components/users"; import { UserProps } from "@/types"; diff --git a/components/users/table/DataTablePagination.tsx b/components/users/table/DataTablePagination.tsx index 0e195f0507..4c4278682f 100644 --- a/components/users/table/DataTablePagination.tsx +++ b/components/users/table/DataTablePagination.tsx @@ -9,7 +9,7 @@ import { import Link from "next/link"; import { usePathname, useSearchParams } from "next/navigation"; -import { extractPaginationInfo } from "@/lib"; +import { getPaginationInfo } from "@/lib"; import { MetaDataProps } from "@/types"; interface DataTablePaginationProps { @@ -22,8 +22,7 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) { const pathname = usePathname(); const searchParams = useSearchParams(); - const { currentPage, totalPages, totalEntries } = - extractPaginationInfo(metadata); + const { currentPage, totalPages, totalEntries } = getPaginationInfo(metadata); const createPageUrl = (pageNumber: number | string) => { const params = new URLSearchParams(searchParams); diff --git a/components/users/table/DataTableUser.tsx b/components/users/table/DataTableUser.tsx index b13093c2ab..68fee28e55 100644 --- a/components/users/table/DataTableUser.tsx +++ b/components/users/table/DataTableUser.tsx @@ -18,7 +18,7 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui"; +} from "@/components/ui/table"; import { MetaDataProps } from "@/types"; import { DataTablePagination } from "./DataTablePagination"; diff --git a/lib/custom.ts b/lib/custom.ts new file mode 100644 index 0000000000..075f6b95c8 --- /dev/null +++ b/lib/custom.ts @@ -0,0 +1,21 @@ +import { MetaDataProps } from "@/types"; + +export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value)); + +export const convertFileToUrl = (file: File) => URL.createObjectURL(file); + +export const getPaginationInfo = (metadata: MetaDataProps) => { + const currentPage = metadata?.pagination.page ?? "1"; + const totalPages = metadata?.pagination.pages; + const totalEntries = metadata?.pagination.count; + + return { currentPage, totalPages, totalEntries }; +}; + +export function encryptKey(passkey: string) { + return btoa(passkey); +} + +export function decryptKey(passkey: string) { + return atob(passkey); +} diff --git a/lib/index.ts b/lib/index.ts index 3ed9e10f8f..8edc14574b 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,2 +1,3 @@ +export * from "./custom"; export * from "./seed"; export * from "./utils"; diff --git a/lib/utils.ts b/lib/utils.ts index c236eefe6a..365058cebd 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,109 +1,6 @@ import { type ClassValue, clsx } from "clsx"; -import { extendTailwindMerge } from "tailwind-merge"; - -const COMMON_UNITS = ["small", "medium", "large"]; -import { MetaDataProps } from "@/types"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -const twMerge = extendTailwindMerge({ - extend: { - theme: { - opacity: ["disabled"], - spacing: ["divider"], - borderWidth: COMMON_UNITS, - borderRadius: COMMON_UNITS, - }, - classGroups: { - shadow: [{ shadow: COMMON_UNITS }], - "font-size": [{ text: ["tiny", ...COMMON_UNITS] }], - "bg-image": ["bg-stripe-gradient"], - }, - }, -}); -export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value)); - -export const convertFileToUrl = (file: File) => URL.createObjectURL(file); - -export const extractPaginationInfo = (metadata: MetaDataProps) => { - const currentPage = metadata?.pagination.page ?? "1"; - const totalPages = metadata?.pagination.pages; - const totalEntries = metadata?.pagination.count; - - return { currentPage, totalPages, totalEntries }; -}; - -// FORMAT DATE TIME -export const formatDateTime = ( - dateString: Date | string, - timeZone: string = Intl.DateTimeFormat().resolvedOptions().timeZone, -) => { - const dateTimeOptions: Intl.DateTimeFormatOptions = { - // weekday: "short", // abbreviated weekday name (e.g., 'Mon') - month: "short", // abbreviated month name (e.g., 'Oct') - day: "numeric", // numeric day of the month (e.g., '25') - year: "numeric", // numeric year (e.g., '2023') - hour: "numeric", // numeric hour (e.g., '8') - minute: "numeric", // numeric minute (e.g., '30') - hour12: true, // use 12-hour clock (true) or 24-hour clock (false), - timeZone: timeZone, // use the provided timezone - }; - - const dateDayOptions: Intl.DateTimeFormatOptions = { - weekday: "short", // abbreviated weekday name (e.g., 'Mon') - year: "numeric", // numeric year (e.g., '2023') - month: "2-digit", // abbreviated month name (e.g., 'Oct') - day: "2-digit", // numeric day of the month (e.g., '25') - timeZone: timeZone, // use the provided timezone - }; - - const dateOptions: Intl.DateTimeFormatOptions = { - month: "short", // abbreviated month name (e.g., 'Oct') - year: "numeric", // numeric year (e.g., '2023') - day: "numeric", // numeric day of the month (e.g., '25') - timeZone: timeZone, // use the provided timezone - }; - - const timeOptions: Intl.DateTimeFormatOptions = { - hour: "numeric", // numeric hour (e.g., '8') - minute: "numeric", // numeric minute (e.g., '30') - hour12: true, // use 12-hour clock (true) or 24-hour clock (false) - timeZone: timeZone, // use the provided timezone - }; - - const formattedDateTime: string = new Date(dateString).toLocaleString( - "en-US", - dateTimeOptions, - ); - - const formattedDateDay: string = new Date(dateString).toLocaleString( - "en-US", - dateDayOptions, - ); - - const formattedDate: string = new Date(dateString).toLocaleString( - "en-US", - dateOptions, - ); - - const formattedTime: string = new Date(dateString).toLocaleString( - "en-US", - timeOptions, - ); - - return { - dateTime: formattedDateTime, - dateDay: formattedDateDay, - dateOnly: formattedDate, - timeOnly: formattedTime, - }; -}; - -export function encryptKey(passkey: string) { - return btoa(passkey); -} - -export function decryptKey(passkey: string) { - return atob(passkey); -} diff --git a/package-lock.json b/package-lock.json index 11d2ea0433..a9c0840142 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "date-fns": "^3.6.0", "framer-motion": "~11.1.1", "intl-messageformat": "^10.5.0", @@ -6308,6 +6309,384 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/cmdk/node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "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/code-block-writer": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", diff --git a/package.json b/package.json index 9a84242353..5e68426395 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "^1.0.0", "date-fns": "^3.6.0", "framer-motion": "~11.1.1", "intl-messageformat": "^10.5.0", diff --git a/styles/globals.css b/styles/globals.css index f3d4bed222..ea9b8d5b5b 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -30,3 +30,10 @@ --chart-5: 340 75% 55%; } } + +@layer utilities { + /* Hide scrollbar */ + .no-scrollbar { + scrollbar-width: none; + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 47d4df9ac4..6d2ff06563 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -19,6 +19,36 @@ module.exports = { }, extend: { colors: { + prowler: { + blue: { + midnight: "#030921", + pale: "#f3fcff", + smoky: "#7b8390", + }, + grey: { + medium: "#353a4d", + light: "#868994", + }, + green: { + DEFAULT: "#9FD655", + medium: "#09BF3D", + }, + theme: { + green: "#6af400", + purple: "#5001d0", + coral: "#ff5356", + orange: "#f69000", + yellow: "#ffdf16", + }, + dark: { + title: "#E2E8F0", + text: "#94a3b8" /* primary default for dark mode */, + }, + light: { + title: "#1e293bff", + text: "#64748b" /* primary default for light mode */, + }, + }, system: { success: { DEFAULT: "#09BF3D", @@ -51,11 +81,53 @@ module.exports = { low: "#fcd34d", }, }, + danger: "#E11D48", }, fontFamily: { sans: ["var(--font-sans)"], mono: ["var(--font-geist-mono)"], }, + boxShadow: { + "box-light": + "-16px -16px 40px rgba(255, 255, 255, 0.8), 16px 4px 64px rgba(18, 61, 101, 0.3), inset -8px -6px 80px rgba(255, 255, 255, 0.18)", + "button-curved-default": + "-4px -2px 16px rgba(255, 255, 255, 0.7), 4px 2px 16px rgba(136, 165, 191, 0.38)", + "button-curved-pressed": + "inset -4px -4px 16px rgba(255, 255, 255, 0.8), inset 4px 4px 12px rgba(136, 165, 191, 0.4)", + "button-flat-nopressed": + "-4px -2px 16px #FFFFFF, 4px 2px 16px rgba(136, 165, 191, 0.48)", + "button-flat-pressed": + "inset -3px -3px 7px #FFFFFF, inset 3px 3px 7px rgba(136, 165, 191, 0.48)", + "box-down-light": + "inset -3px -3px 7px #FFFFFF, inset 2px 2px 5px rgba(136, 165, 191, 0.38)", + "box-up": + "-4px -2px 16px #FFFFFF, 4px 2px 16px rgba(136, 165, 191, 0.54)", + "box-dark": + "-4px -2px 16px rgba(195, 200, 205, 0.09), 4px 4px 18px rgba(0, 0, 0, 0.5)", + "box-dark-out": "inset 2px 2px 2px rgba(26, 32, 38, 0.4)", + "buttons-box-dark": + "-5px -6px 16px rgba(195, 200, 205, 0.04), 22px 22px 60px rgba(0, 0, 0, 0.5)", + "button-curved-default-dark": + "-4px -4px 16px rgba(195, 200, 205, 0.06), 4px 4px 18px rgba(0, 0, 0, 0.6)", + "button-curved-pressed-dark": + "-4px -2px 16px rgba(195, 200, 205, 0.07), 4px 4px 18px rgba(0, 0, 0, 0.44)", + "sky-light": + "-16px 20px 40px rgba(215, 215, 215, 0.3), -2px 2px 24px rgba(22, 28, 47, 0.3), -16px 28px 120px rgba(0, 0, 0, 0.1)", + "midnight-dark": + "-16px 20px 40px rgba(3, 9, 33, 0.3), -2px 2px 24px rgba(3, 9, 33, 0.6), -16px 28px 120px rgba(3, 9, 33, 0.1)", + switcher: + "0px -6px 24px #FFFFFF, 0px 7px 16px rgba(104, 132, 157, 0.5)", + up: "0.3rem 0.3rem 0.6rem #c8d0e7, -0.2rem -0.2rem 0.5rem #fff", + down: "inset 0.2rem 0.2rem 0.5rem #c8d0e7, inset -0.2rem -0.2rem 0.5rem #fff", + }, + animation: { + "fade-in": "fade-in 200ms ease-out 0s 1 normal forwards running", + "fade-out": "fade-out 200ms ease-in 0s 1 normal forwards running", + expand: "expand 400ms linear 0s 1 normal forwards running", + "slide-in": "slide-in 400ms linear 0s 1 normal forwards running", + "slide-out": "slide-out 400ms linear 0s 1 normal forwards running", + collapse: "collapse 400ms linear 0s 1 normal forwards running", + }, keyframes: { "accordion-down": { from: { height: "0" }, diff --git a/types/components.ts b/types/components.ts index bd2502a44b..28ffbe7355 100644 --- a/types/components.ts +++ b/types/components.ts @@ -9,6 +9,23 @@ export type IconProps = { style?: React.CSSProperties; }; +export type NextUIVariants = + | "solid" + | "faded" + | "bordered" + | "light" + | "flat" + | "ghost" + | "shadow"; + +export type NextUIColors = + | "primary" + | "secondary" + | "success" + | "warning" + | "danger" + | "default"; + export interface SearchParamsProps { [key: string]: string | string[] | undefined; } @@ -16,8 +33,8 @@ export interface ProviderProps { id: string; type: "providers"; attributes: { - provider: "aws" | "azure" | "gcp"; - provider_id: string; + provider: "aws" | "azure" | "gcp" | "kubernetes"; + uid: string; alias: string; status: "completed" | "pending" | "cancelled"; resources: number; diff --git a/types/filters.ts b/types/filters.ts new file mode 100644 index 0000000000..944706f7aa --- /dev/null +++ b/types/filters.ts @@ -0,0 +1,20 @@ +export interface FilterOption { + key: string; + labelCheckboxGroup: string; + values: string[]; +} + +export interface CustomDropdownFilterProps { + filter: FilterOption; + onFilterChange: (key: string, values: string[]) => void; +} + +export interface FilterControlsProps { + search?: boolean; + providers?: boolean; + date?: boolean; + regions?: boolean; + accounts?: boolean; + mutedFindings?: boolean; + customFilters?: FilterOption[]; +} diff --git a/types/formSchemas.ts b/types/formSchemas.ts index ee174f95d0..3726245f68 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -1,5 +1,11 @@ import { z } from "zod"; +export const addProviderFormSchema = z.object({ + providerType: z.string(), + providerAlias: z.string(), + providerId: z.string(), +}); + export const editProviderFormSchema = (currentAlias: string) => z.object({ alias: z diff --git a/types/index.ts b/types/index.ts index 955dd0d6f8..c247bc2080 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,3 +1,4 @@ export * from "./authFormSchema"; export * from "./components"; +export * from "./filters"; export * from "./formSchemas";