From 1dfde958bfe4e5f48900e1654a11e782e1c06b8f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sun, 22 Sep 2024 15:48:03 +0200 Subject: [PATCH 1/5] chore: rename getProviders action and add modal for editing provider info --- actions/providers.ts | 27 +++++++++++++++++-- app/(prowler)/providers/page.tsx | 4 +-- .../table/data-table-row-actions.tsx | 13 +++++---- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/actions/providers.ts b/actions/providers.ts index 380ed72284..68478dc4d2 100644 --- a/actions/providers.ts +++ b/actions/providers.ts @@ -6,7 +6,7 @@ import { redirect } from "next/navigation"; import { auth } from "@/auth.config"; import { parseStringify } from "@/lib"; -export const getProvider = async ({ +export const getProviders = async ({ page = 1, query = "", sort = "", @@ -47,6 +47,28 @@ export const getProvider = async ({ } }; +export const getProvider = async (formData: FormData) => { + const keyServer = process.env.API_BASE_URL; + + const providerId = formData.get("id"); + + try { + const response = await fetch(`${keyServer}/providers/${providerId}`, { + method: "GET", + 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 addProvider = async (formData: FormData) => { const keyServer = process.env.API_BASE_URL; @@ -81,8 +103,8 @@ export const addProvider = async (formData: FormData) => { error: getErrorMessage(error), }; } - revalidatePath("/providers"); }; + export const checkConnectionProvider = async (formData: FormData) => { const keyServer = process.env.API_BASE_URL; @@ -107,6 +129,7 @@ export const checkConnectionProvider = async (formData: FormData) => { }; } }; + export const deleteProvider = async (formData: FormData) => { const keyServer = process.env.API_BASE_URL; diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index 79ccbf5d29..6a79a8a872 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -1,7 +1,7 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; -import { getProvider } from "@/actions"; +import { getProviders } from "@/actions"; import { FilterControls } from "@/components/filters"; import { AddProviderModal } from "@/components/providers"; import { @@ -54,7 +54,7 @@ const SSRDataTable = async ({ // Extract query from filters const query = (filters["filter[search]"] as string) || ""; - const providersData = await getProvider({ query, page, sort, filters }); + const providersData = await getProviders({ query, page, sort, filters }); return ( { row: Row; @@ -32,18 +33,19 @@ const iconClasses = export function DataTableRowActions({ row, }: DataTableRowActionsProps) { - // const [isEditOpen, setIsEditOpen] = useState(false); + const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const providerId = (row.original as { id: string }).id; return ( <> - {/* - */} + ({ shortcut="⌘⇧E" textValue="Edit Provider" startContent={} + onClick={() => setIsEditOpen(true)} > Edit Provider From 3b96d14f84f973191744a982dc9f194b2f37c4ef Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sun, 22 Sep 2024 15:48:47 +0200 Subject: [PATCH 2/5] chore: rename getProviders action and add modal for editing provider info --- actions/providers.ts | 50 +++++++- components/providers/forms/DeleteForm.tsx | 2 +- components/providers/forms/EditForm.tsx | 108 ++++++++++++++++++ components/providers/forms/index.ts | 1 + .../table/data-table-row-actions.tsx | 9 +- 5 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 components/providers/forms/EditForm.tsx diff --git a/actions/providers.ts b/actions/providers.ts index 68478dc4d2..178ece1df1 100644 --- a/actions/providers.ts +++ b/actions/providers.ts @@ -48,21 +48,61 @@ export const getProviders = async ({ }; export const getProvider = async (formData: FormData) => { - const keyServer = process.env.API_BASE_URL; - + const session = await auth(); + const tenantId = session?.user.tenantId; const providerId = formData.get("id"); + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/providers/${providerId}`); + try { - const response = await fetch(`${keyServer}/providers/${providerId}`, { - method: "GET", + const providers = await fetch(url.toString(), { headers: { - "X-Tenant-ID": `${process.env.HEADER_TENANT_ID}`, + "X-Tenant-ID": `${tenantId}`, }, }); + const data = await providers.json(); + const parsedData = parseStringify(data); + return parsedData; + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; + +export const updateProvider = 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 alias = formData.get("alias"); + + const url = new URL(`${keyServer}/providers/${providerId}`); + + try { + const response = await fetch(url.toString(), { + method: "PATCH", + headers: { + "X-Tenant-ID": `${tenantId}`, + "Content-Type": "application/vnd.api+json", + }, + body: JSON.stringify({ + data: { + type: "Provider", + id: providerId, + attributes: { + alias: alias, + }, + }, + }), + }); const data = await response.json(); revalidatePath("/providers"); return parseStringify(data); } catch (error) { + console.error(error); return { error: getErrorMessage(error), }; diff --git a/components/providers/forms/DeleteForm.tsx b/components/providers/forms/DeleteForm.tsx index b739299fb8..2a9d79902c 100644 --- a/components/providers/forms/DeleteForm.tsx +++ b/components/providers/forms/DeleteForm.tsx @@ -59,7 +59,7 @@ export const DeleteForm = ({ disabled={isLoading} className="w-full hidden sm:block" type="button" - onClick={() => setIsOpen(false)} + onPress={() => setIsOpen(false)} > Cancel diff --git a/components/providers/forms/EditForm.tsx b/components/providers/forms/EditForm.tsx new file mode 100644 index 0000000000..55774a1b57 --- /dev/null +++ b/components/providers/forms/EditForm.tsx @@ -0,0 +1,108 @@ +"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 { updateProvider } from "@/actions"; +import { useToast } from "@/components/ui"; +import { CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; + +const formSchema = z.object({ + alias: z.string(), + providerId: z.string(), +}); + +export const EditForm = ({ + providerId, + providerAlias, + setIsOpen, +}: { + providerId: string; + providerAlias?: string; + setIsOpen: Dispatch>; +}) => { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + alias: "", + }, + }); + + const { toast } = useToast(); + const isLoading = form.formState.isSubmitting; + + async function onSubmitClient(formData: FormData) { + // client-side validation + 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 provider was updated successfully.", + }); + } + } + + return ( +
+ + +
Current alias: {providerAlias}
+ + + +
+ + + +
+ + + ); +}; diff --git a/components/providers/forms/index.ts b/components/providers/forms/index.ts index 005f96941f..af1cdc1b37 100644 --- a/components/providers/forms/index.ts +++ b/components/providers/forms/index.ts @@ -1 +1,2 @@ export * from "./DeleteForm"; +export * from "./EditForm"; diff --git a/components/providers/table/data-table-row-actions.tsx b/components/providers/table/data-table-row-actions.tsx index 70c461a919..5b8067e0e8 100644 --- a/components/providers/table/data-table-row-actions.tsx +++ b/components/providers/table/data-table-row-actions.tsx @@ -21,8 +21,8 @@ import { VerticalDotsIcon } from "@/components/icons"; import { CustomAlertModal } from "@/components/ui/custom"; import { CheckConnectionProvider } from "../CheckConnectionProvider"; +import { EditForm } from "../forms"; import { DeleteForm } from "../forms/DeleteForm"; -import EditForm from "../forms/EditForm"; interface DataTableRowActionsProps { row: Row; @@ -36,6 +36,7 @@ export function DataTableRowActions({ 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 ( <> ({ title="Edit Provider" description={"Edit the provider details"} > - + Date: Tue, 24 Sep 2024 08:09:02 +0200 Subject: [PATCH 3/5] feat: edit provider has client validation now --- actions/providers.ts | 6 ++-- components/providers/forms/EditForm.tsx | 45 +++++++++++++++---------- components/ui/custom/CustomInputNew.tsx | 44 ++++++++++++++++++++++++ components/ui/custom/index.ts | 1 + types/{auth.ts => authFormSchema.ts} | 0 types/formSchemas.ts | 15 +++++++++ types/index.ts | 3 +- 7 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 components/ui/custom/CustomInputNew.tsx rename types/{auth.ts => authFormSchema.ts} (100%) create mode 100644 types/formSchemas.ts diff --git a/actions/providers.ts b/actions/providers.ts index 178ece1df1..d4b0dc4d29 100644 --- a/actions/providers.ts +++ b/actions/providers.ts @@ -76,8 +76,8 @@ export const updateProvider = async (formData: FormData) => { const keyServer = process.env.API_BASE_URL; const tenantId = session?.user.tenantId; - const providerId = formData.get("id"); - const alias = formData.get("alias"); + const providerId = formData.get("providerId"); + const providerAlias = formData.get("alias"); const url = new URL(`${keyServer}/providers/${providerId}`); @@ -93,7 +93,7 @@ export const updateProvider = async (formData: FormData) => { type: "Provider", id: providerId, attributes: { - alias: alias, + alias: providerAlias, }, }, }), diff --git a/components/providers/forms/EditForm.tsx b/components/providers/forms/EditForm.tsx index 55774a1b57..1404382296 100644 --- a/components/providers/forms/EditForm.tsx +++ b/components/providers/forms/EditForm.tsx @@ -8,13 +8,9 @@ import * as z from "zod"; import { updateProvider } from "@/actions"; import { useToast } from "@/components/ui"; -import { CustomInput } from "@/components/ui/custom"; +import { CustomInputNew } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; - -const formSchema = z.object({ - alias: z.string(), - providerId: z.string(), -}); +import { editProviderFormSchema } from "@/types"; export const EditForm = ({ providerId, @@ -25,18 +21,26 @@ export const EditForm = ({ providerAlias?: string; setIsOpen: Dispatch>; }) => { + const formSchema = editProviderFormSchema(providerAlias ?? ""); + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - alias: "", + providerId: providerId, + alias: providerAlias, }, }); const { toast } = useToast(); const isLoading = form.formState.isSubmitting; - async function onSubmitClient(formData: FormData) { - // client-side validation + 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) { @@ -54,18 +58,27 @@ export const EditForm = ({ description: "The provider was updated successfully.", }); } - } + }; return (
- +
Current alias: {providerAlias}
- */} + + setIsOpen(false)} > {isLoading ? ( - <> - - Saving... - + ) : ( Save )} diff --git a/components/ui/custom/CustomInputNew.tsx b/components/ui/custom/CustomInputNew.tsx new file mode 100644 index 0000000000..a5f16436be --- /dev/null +++ b/components/ui/custom/CustomInputNew.tsx @@ -0,0 +1,44 @@ +import { Input } from "@nextui-org/react"; +import { Control, FieldPath, FieldValues } from "react-hook-form"; + +import { FormControl, FormField, FormMessage } from "@/components/ui/form"; + +interface CustomInputNewProps { + control: Control; + name: FieldPath; + label: string; + type: string; + placeholder?: string; + isRequired?: boolean; +} + +export const CustomInputNew = ({ + control, + name, + label, + type, + placeholder, + isRequired = false, +}: CustomInputNewProps) => { + return ( + ( + <> + + + + + + )} + /> + ); +}; diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts index 0520404750..d36dafac6e 100644 --- a/components/ui/custom/index.ts +++ b/components/ui/custom/index.ts @@ -2,4 +2,5 @@ export * from "./CustomAlertModal"; export * from "./CustomBox"; export * from "./CustomButtonClientAction"; export * from "./CustomInput"; +export * from "./CustomInputNew"; export * from "./CustomLoader"; diff --git a/types/auth.ts b/types/authFormSchema.ts similarity index 100% rename from types/auth.ts rename to types/authFormSchema.ts diff --git a/types/formSchemas.ts b/types/formSchemas.ts new file mode 100644 index 0000000000..ee174f95d0 --- /dev/null +++ b/types/formSchemas.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const editProviderFormSchema = (currentAlias: string) => + z.object({ + alias: z + .string() + .refine((val) => val === "" || val.length >= 3, { + message: "The alias must be empty or have at least 3 characters.", + }) + .refine((val) => val !== currentAlias, { + message: "The new alias must be different from the current one.", + }) + .optional(), + providerId: z.string(), + }); diff --git a/types/index.ts b/types/index.ts index 89e43941c9..955dd0d6f8 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,2 +1,3 @@ -export * from "./auth"; +export * from "./authFormSchema"; export * from "./components"; +export * from "./formSchemas"; From 94eba806e3b1d1aeef7aa697ab0874ad164fe8a8 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 24 Sep 2024 08:40:48 +0200 Subject: [PATCH 4/5] feat: big refactor for CustomInput component --- components/auth/AuthForm.tsx | 2 +- components/providers/forms/EditForm.tsx | 12 ++----- components/ui/custom/CustomInput.tsx | 43 ++++++++---------------- components/ui/custom/CustomInputNew.tsx | 44 ------------------------- components/ui/custom/index.ts | 1 - 5 files changed, 17 insertions(+), 85 deletions(-) delete mode 100644 components/ui/custom/CustomInputNew.tsx diff --git a/components/auth/AuthForm.tsx b/components/auth/AuthForm.tsx index 797d076b5f..11176f0aab 100644 --- a/components/auth/AuthForm.tsx +++ b/components/auth/AuthForm.tsx @@ -126,7 +126,7 @@ export const AuthForm = ({ type }: { type: string }) => { placeholder="Enter your email" /> - + {type === "sign-in" && (
diff --git a/components/providers/forms/EditForm.tsx b/components/providers/forms/EditForm.tsx index 1404382296..2b82b7116d 100644 --- a/components/providers/forms/EditForm.tsx +++ b/components/providers/forms/EditForm.tsx @@ -8,7 +8,7 @@ import * as z from "zod"; import { updateProvider } from "@/actions"; import { useToast } from "@/components/ui"; -import { CustomInputNew } from "@/components/ui/custom"; +import { CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { editProviderFormSchema } from "@/types"; @@ -70,15 +70,7 @@ export const EditForm = ({
Current alias: {providerAlias}
- {/* */} - - >; +interface CustomInputProps { + control: Control; + name: FieldPath; + label?: string; + type?: string; + placeholder?: string; isRequired?: boolean; -} & ( - | { - password?: false; - name: FieldPath>; - label: string; - type: "text" | "email"; - placeholder: string; - } - | { - password: true; - name?: never; - label?: never; - type?: never; - placeholder?: never; - } -); + password?: boolean; +} -export const CustomInput = ({ +export const CustomInput = ({ control, name, - type, - label, + type = "text", + label = name, placeholder, password = false, isRequired = true, -}: CustomInputProps) => { +}: CustomInputProps) => { const [isVisible, setIsVisible] = useState(false); const toggleVisibility = () => setIsVisible(!isVisible); - const inputName = password ? "password" : name!; const inputLabel = password ? "Password" : label; const inputType = password ? (isVisible ? "text" : "password") : type; const inputPlaceholder = password ? "Enter your password" : placeholder; @@ -62,7 +47,7 @@ export const CustomInput = ({ return ( >} + name={name} render={({ field }) => ( <> diff --git a/components/ui/custom/CustomInputNew.tsx b/components/ui/custom/CustomInputNew.tsx deleted file mode 100644 index a5f16436be..0000000000 --- a/components/ui/custom/CustomInputNew.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Input } from "@nextui-org/react"; -import { Control, FieldPath, FieldValues } from "react-hook-form"; - -import { FormControl, FormField, FormMessage } from "@/components/ui/form"; - -interface CustomInputNewProps { - control: Control; - name: FieldPath; - label: string; - type: string; - placeholder?: string; - isRequired?: boolean; -} - -export const CustomInputNew = ({ - control, - name, - label, - type, - placeholder, - isRequired = false, -}: CustomInputNewProps) => { - return ( - ( - <> - - - - - - )} - /> - ); -}; diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts index d36dafac6e..0520404750 100644 --- a/components/ui/custom/index.ts +++ b/components/ui/custom/index.ts @@ -2,5 +2,4 @@ export * from "./CustomAlertModal"; export * from "./CustomBox"; export * from "./CustomButtonClientAction"; export * from "./CustomInput"; -export * from "./CustomInputNew"; export * from "./CustomLoader"; From b860e35408bddb689f57978a2fd400543e2aa4f9 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 24 Sep 2024 11:43:51 +0200 Subject: [PATCH 5/5] feat: fuctionality tweaks handling errors --- components/providers/forms/EditForm.tsx | 29 ++++++++++++++--------- components/ui/custom/CustomAlertModal.tsx | 4 ++-- components/ui/custom/CustomInput.tsx | 12 ++++++++-- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/components/providers/forms/EditForm.tsx b/components/providers/forms/EditForm.tsx index 2b82b7116d..8479aeeabb 100644 --- a/components/providers/forms/EditForm.tsx +++ b/components/providers/forms/EditForm.tsx @@ -57,26 +57,33 @@ export const EditForm = ({ title: "Success!", description: "The provider was updated successfully.", }); + setIsOpen(false); // Close the modal on success } }; return ( +
+ Current alias: {providerAlias} +
+
+ +
-
Current alias: {providerAlias}
- -