feat: add new functionality for adding provider and remove the old one

This commit is contained in:
Pablo Lara
2024-09-27 15:33:36 +02:00
parent 2807fc2b8e
commit f7df63e2af
15 changed files with 310 additions and 292 deletions
+4 -5
View File
@@ -117,15 +117,14 @@ export const updateProvider = async (formData: FormData) => {
};
export const addProvider = async (formData: FormData) => {
console.log(formData);
const session = await auth();
const keyServer = process.env.API_BASE_URL;
const tenantId = session?.user.tenantId;
const provider = formData.get("provider");
const providerType = formData.get("providerType");
const providerId = formData.get("providerId");
const alias = formData.get("alias");
const providerAlias = formData.get("providerAlias");
const url = new URL(`${keyServer}/providers`);
@@ -141,9 +140,9 @@ export const addProvider = async (formData: FormData) => {
data: {
type: "Provider",
attributes: {
provider: provider,
provider: providerType,
uid: providerId,
alias: alias,
alias: providerAlias,
},
},
}),
+10 -10
View File
@@ -3,7 +3,7 @@ import { Suspense } from "react";
import { getProviders } from "@/actions";
import { FilterControls } from "@/components/filters";
import { AddProviderModal } from "@/components/providers";
import { AddProvider } from "@/components/providers";
import {
ColumnsProvider,
DataTableProvider,
@@ -22,18 +22,18 @@ export default async function Providers({
return (
<>
<Header title="Providers" icon="fluent:cloud-sync-24-regular" />
<Spacer y={4} />
<FilterControls search providers />
<Spacer y={4} />
<div className="flex flex-col items-end w-full">
<div className="flex space-x-6">
<AddProviderModal />
</div>
<Spacer y={6} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableProvider />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
</div>
<AddProvider />
<Spacer y={4} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableProvider />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
{/* <NewTable /> */}
</>
);
}
+26
View File
@@ -757,3 +757,29 @@ export const SaveIcon: React.FC<IconSvgProps> = ({
</svg>
);
};
export const AddIcon: React.FC<IconSvgProps> = ({
size,
height,
width,
...props
}) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
height={size || height || 20}
viewBox="0 0 24 24"
width={size || width || 20}
aria-hidden="true"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10m.75-13a.75.75 0 0 0-1.5 0v2.25H9a.75.75 0 0 0 0 1.5h2.25V15a.75.75 0 0 0 1.5 0v-2.25H15a.75.75 0 0 0 0-1.5h-2.25z"
clipRule="evenodd"
/>
</svg>
);
};
-63
View File
@@ -1,63 +0,0 @@
"use client";
import { useRef } from "react";
import { addProvider } from "@/actions";
import { useToast } from "../ui/toast";
import { ButtonAddProvider } from "./ButtonAddProvider";
export const AddProvider = () => {
const ref = useRef<HTMLFormElement>(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 (
<form ref={ref} action={clientAction} className="flex gap-x-2">
<input
type="text"
name="provider"
placeholder="Provider"
aria-label="Provider"
className="py-2 px-3 rounded-sm"
/>
<input
type="text"
name="id"
placeholder="Provider ID"
aria-label="Provider ID"
className="py-2 px-3 rounded-sm"
/>
<input
type="text"
name="alias"
placeholder="Alias"
aria-label="Alias"
className="py-2 px-3 rounded-sm"
/>
<ButtonAddProvider />
</form>
);
};
-102
View File
@@ -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<HTMLFormElement>(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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button aria-label="Add Cloud Account" variant="ghost">
Add Cloud Account
</Button>
</DialogTrigger>
<DialogContent className="flex flex-col sm:max-w-md md:max-w-4xl">
<DialogHeader className="mb-6 space-y-3">
<DialogTitle className="text-2xl text-center">
Add cloud account
</DialogTitle>
<DialogDescription className="text-md">
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:
</DialogDescription>
</DialogHeader>
<form
ref={ref}
action={clientAction}
onSubmit={() => setOpen(false)}
className="grid sm:grid-cols-2 gap-6"
>
<div className="col-span-1">
<CustomRadioProvider />
</div>
<div className="col-span-1 flex flex-col gap-y-2 my-auto">
<Input
type="text"
name="id"
label="Provider ID"
labelPlacement="outside"
placeholder="Provider ID"
className="w-full rounded-sm"
aria-label="Enter Provider ID"
/>
<Input
type="text"
name="alias"
label="Alias"
labelPlacement="outside"
placeholder="alias"
className="w-full rounded-sm"
aria-label="Enter Provider alias"
/>
</div>
<div className="col-span-2 flex justify-center mt-4">
<ButtonAddProvider />
</div>
</form>
</DialogContent>
</Dialog>
);
};
@@ -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 (
<Button type="submit" area-disabled={pending}>
{pending ? <CircularProgress aria-label="Loading..." /> : "Add provider"}
</Button>
);
};
+48 -21
View File
@@ -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<CustomRadioProps> = (props) => {
);
};
export const CustomRadioProvider = () => {
interface CustomRadioProviderProps {
control: Control<z.infer<typeof addProviderFormSchema>>;
}
export const CustomRadioProvider: React.FC<CustomRadioProviderProps> = ({
control,
}) => {
return (
<RadioGroup label="Select one provider" name="provider">
<CustomRadio description="Amazon Web Services" value="aws">
<div className="flex items-center">
<AWSProviderBadge size={26} />
<span className="ml-2">AWS</span>
</div>
</CustomRadio>
<CustomRadio description="Google Cloud Platform" value="gcp">
<div className="flex items-center">
<GCPProviderBadge size={26} />
<span className="ml-2">GCP</span>
</div>
</CustomRadio>
<CustomRadio description="Microsoft Azure" value="azure">
<div className="flex items-center">
<AzureProviderBadge size={26} />
<span className="ml-2">Azure</span>
</div>
</CustomRadio>
</RadioGroup>
<Controller
name="providerType"
control={control}
render={({ field }) => (
<>
<RadioGroup label="Select one provider" {...field}>
<CustomRadio description="Amazon Web Services" value="aws">
<div className="flex items-center">
<AWSProviderBadge size={26} />
<span className="ml-2">AWS</span>
</div>
</CustomRadio>
<CustomRadio description="Google Cloud Platform" value="gcp">
<div className="flex items-center">
<GCPProviderBadge size={26} />
<span className="ml-2">GCP</span>
</div>
</CustomRadio>
<CustomRadio description="Microsoft Azure" value="azure">
<div className="flex items-center">
<AzureProviderBadge size={26} />
<span className="ml-2">Azure</span>
</div>
</CustomRadio>
<CustomRadio description="Kubernetes" value="kubernetes">
<div className="flex items-center">
<KS8ProviderBadge size={26} />
<span className="ml-2">Kubernetes</span>
</div>
</CustomRadio>
</RadioGroup>
<FormMessage className="text-system-error dark:text-system-error" />
</>
)}
/>
);
};
+49
View File
@@ -0,0 +1,49 @@
"use client";
import { Chip } from "@nextui-org/react";
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 (
<>
<CustomAlertModal
isOpen={isAddOpen}
onOpenChange={setIsAddOpen}
title="Add Cloud Provider"
description={
"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:"
}
>
<AddForm setIsOpen={setIsAddOpen} />
</CustomAlertModal>
<div className="w-full flex items-center justify-between">
<div className="flex w-fit items-center gap-2">
<h1 className="text-2xl font-[700] leading-[32px]">TBD</h1>
<Chip
className="hidden items-center text-default-500 sm:flex"
size="sm"
variant="flat"
>
3
</Chip>
</div>
<CustomButton
variant="solid"
color="action"
size="md"
onPress={() => setIsAddOpen(true)}
endContent={<AddIcon size={20} />}
>
Add Account
</CustomButton>
</div>
</>
);
};
+123
View File
@@ -0,0 +1,123 @@
"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<SetStateAction<boolean>>;
}) => {
const formSchema = addProviderFormSchema;
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerType: "",
providerId: "",
providerAlias: "",
},
});
const { toast } = useToast();
const isLoading = form.formState.isSubmitting;
const onSubmitClient = async (values: z.infer<typeof formSchema>) => {
console.log(values);
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<CustomRadioProvider control={form.control} />
<CustomInput
control={form.control}
name="providerId"
type="text"
label="Provider ID"
labelPlacement="inside"
placeholder={"Enter the provider ID"}
variant="bordered"
isRequired
isInvalid={!!form.formState.errors.providerId}
/>
<CustomInput
control={form.control}
name="providerAlias"
type="text"
label="Alias"
labelPlacement="inside"
placeholder={"Enter the provider alias"}
variant="bordered"
isRequired={false}
isInvalid={!!form.formState.errors.providerAlias}
/>
<div className="w-full flex justify-center sm:space-x-6">
<CustomButton
type="button"
className="w-full bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={() => setIsOpen(false)}
disabled={isLoading}
>
<span>Cancel</span>
</CustomButton>
<CustomButton
type="submit"
className="w-full"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
startContent={!isLoading && <SaveIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Confirm</span>}
</CustomButton>
</div>
</form>
</Form>
);
};
+1
View File
@@ -1,2 +1,3 @@
export * from "./AddForm";
export * from "./DeleteForm";
export * from "./EditForm";
+1 -3
View File
@@ -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";
@@ -1,18 +1,21 @@
"use client";
import { Button } from "@nextui-org/react";
// import {
// Button,
// Input,
// Popover,
// PopoverContent,
// PopoverTrigger,
// RadioGroup,
// } from "@nextui-org/react";
// import { Icon, Radio, SearchIcon } from "lucide-react";
import {
Button,
Checkbox,
CheckboxGroup,
Input,
Popover,
PopoverContent,
PopoverTrigger,
} from "@nextui-org/react";
import { SearchIcon } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useMemo } from "react";
import { CustomFilterIcon, PlusCircleIcon } from "@/components/icons";
import { CustomButton } from "@/components/ui/custom";
interface DataTableFilterCustomProps {
filters: { key: string; values: string[] }[];
}
@@ -53,7 +56,7 @@ export function DataTableFilterCustom({ filters }: DataTableFilterCustomProps) {
return (
<>
{/* <div className="flex items-center gap-4 overflow-auto px-[6px] py-[4px]">
<div className="flex items-center gap-4 overflow-auto px-[6px] py-[4px]">
<div className="flex items-center gap-3">
<div className="flex items-center gap-4">
<Input
@@ -70,60 +73,50 @@ export function DataTableFilterCustom({ filters }: DataTableFilterCustomProps) {
<div>
<Popover placement="bottom">
<PopoverTrigger>
<Button className="bg-default-100 text-default-800" size="sm">
<Button
className="bg-default-100 text-default-800"
startContent={
<PlusCircleIcon className="text-default-400" width={16} />
}
size="sm"
>
Filter
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="flex w-full flex-col gap-6 px-2 py-4">
<RadioGroup
label="Worker Type"
// value={workerTypeFilter}
// onValueChange={setWorkerTypeFilter}
<CheckboxGroup
label="Select cities"
defaultValue={["buenos-aires", "london"]}
>
<Radio value="all">All</Radio>
<Radio value="employee">Employee</Radio>
<Radio value="contractor">Contractor</Radio>
</RadioGroup>
<RadioGroup
label="Status"
// value={statusFilter}
// onValueChange={setStatusFilter}
>
<Radio value="all">All</Radio>
<Radio value="active">Active</Radio>
<Radio value="inactive">Inactive</Radio>
<Radio value="paused">Paused</Radio>
<Radio value="vacation">Vacation</Radio>
</RadioGroup>
<RadioGroup
label="Start Date"
// value={startDateFilter}
// onValueChange={setStartDateFilter}
>
<Radio value="all">All</Radio>
<Radio value="last7Days">Last 7 days</Radio>
<Radio value="last30Days">Last 30 days</Radio>
<Radio value="last60Days">Last 60 days</Radio>
</RadioGroup>
<Checkbox value="buenos-aires">Buenos Aires</Checkbox>
<Checkbox value="sydney">Sydney</Checkbox>
<Checkbox value="san-francisco">San Francisco</Checkbox>
<Checkbox value="london">London</Checkbox>
<Checkbox value="tokyo">Tokyo</Checkbox>
</CheckboxGroup>
</div>
</PopoverContent>
</Popover>
</div>
</div>
</div> */}
</div>
<div className="flex flex-wrap items-center space-x-2">
<CustomButton
variant="dashed"
size="sm"
startContent={<CustomFilterIcon size={16} />}
>
Show Filters
</CustomButton>
{filters.flatMap(({ key, values }) =>
values.map((value) => (
<Button
key={`${key}-${value}`}
onClick={() => applyFilter(key, value)}
onPress={() => applyFilter(key, value)}
// eslint-disable-next-line security/detect-object-injection
variant={activeFilters[key] === value ? "faded" : "light"}
size="sm"
>
{value || "All"}
</Button>
@@ -1,6 +1,5 @@
"use client";
import { Icon } from "@iconify/react";
import { Button, Chip } from "@nextui-org/react";
import {
ColumnDef,
ColumnFiltersState,
@@ -12,7 +11,7 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { useMemo, useState } from "react";
import { useState } from "react";
import {
Table,
@@ -65,33 +64,9 @@ export function DataTableProvider<TData, TValue>({
{ key: "connected", values: ["false", "true"] },
// Add more filter categories as needed
];
const topBar = useMemo(() => {
return (
<div className="mb-[18px] w-full flex items-center justify-between">
<div className="flex w-fit items-center gap-2">
<h1 className="text-2xl font-[700] leading-[32px]">Providers</h1>
<Chip
className="hidden items-center text-default-500 sm:flex"
size="sm"
variant="flat"
>
3
</Chip>
</div>
<Button
color="primary"
endContent={<Icon icon="solar:add-circle-bold" width={20} />}
>
Add Account
</Button>
</div>
);
}, []);
return (
<>
{topBar}
<DataTableFilterCustom filters={filters} />
<div className="p-4 z-0 flex flex-col relative justify-between gap-4 bg-content1 overflow-auto rounded-large shadow-small w-full ">
+1 -1
View File
@@ -33,7 +33,7 @@ export interface ProviderProps {
id: string;
type: "providers";
attributes: {
provider: "aws" | "azure" | "gcp";
provider: "aws" | "azure" | "gcp" | "kubernetes";
uid: string;
alias: string;
status: "completed" | "pending" | "cancelled";
+6
View File
@@ -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