diff --git a/components/dashboard/settings/cluster/registry/add-docker-registry.tsx b/components/dashboard/settings/cluster/registry/add-docker-registry.tsx new file mode 100644 index 000000000..efef6ca89 --- /dev/null +++ b/components/dashboard/settings/cluster/registry/add-docker-registry.tsx @@ -0,0 +1,193 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AlertTriangle, Container } from "lucide-react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const AddRegistrySchema = z.object({ + registryName: z.string().min(1, { + message: "Registry name is required", + }), + username: z.string().min(1, { + message: "Username is required", + }), + password: z.string().min(1, { + message: "Password is required", + }), + registryUrl: z.string().min(1, { + message: "Registry URL is required", + }), +}); + +type AddRegistry = z.infer; + +export const AddRegistry = () => { + const utils = api.useUtils(); + const [isOpen, setIsOpen] = useState(false); + const { mutateAsync, error, isError } = api.project.create.useMutation(); + const router = useRouter(); + const form = useForm({ + defaultValues: { + username: "", + password: "", + registryUrl: "", + }, + resolver: zodResolver(AddRegistrySchema), + }); + + useEffect(() => { + form.reset({ + username: "", + password: "", + registryUrl: "", + }); + }, [form, form.reset, form.formState.isSubmitSuccessful]); + + const onSubmit = async (data: AddRegistry) => { + // await mutateAsync({ + // name: data.name, + // description: data.description, + // }) + // .then(async (data) => { + // await utils.project.all.invalidate(); + // toast.success("Project Created"); + // setIsOpen(false); + // router.push(`/dashboard/project/${data.projectId}`); + // }) + // .catch(() => { + // toast.error("Error to create a project"); + // }); + }; + + return ( + + + + + + + Add a external registry + + Fill the next fields to add a external registry. + + + {isError && ( +
+ + + {error?.message} + +
+ )} +
+ +
+ ( + + Registry Name + + + + + + + )} + /> +
+
+ ( + + Username + + + + + + + )} + /> +
+
+ ( + + Password + + + + + + + )} + /> +
+
+ ( + + Registry URL + + + + + + + )} + /> +
+ + + +
+ +
+
+ ); +}; diff --git a/components/dashboard/settings/cluster/registry/add-self-registry.tsx b/components/dashboard/settings/cluster/registry/add-self-registry.tsx new file mode 100644 index 000000000..ee82487d5 --- /dev/null +++ b/components/dashboard/settings/cluster/registry/add-self-registry.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { api } from "@/utils/api"; +import { toast } from "sonner"; + +export const AddSelfRegistry = () => { + return ( + + + + + + + Are you absolutely sure? + + This will setup a self hosted registry. + + + + Cancel + { + // await mutateAsync({ + // authId, + // }) + // .then(async () => { + // utils.user.all.invalidate(); + // toast.success("User delete succesfully"); + // }) + // .catch(() => { + // toast.error("Error to delete the user"); + // }); + }} + > + Confirm + + + + + ); +}; diff --git a/components/dashboard/settings/cluster/registry/show-registry.tsx b/components/dashboard/settings/cluster/registry/show-registry.tsx new file mode 100644 index 000000000..4e85c9946 --- /dev/null +++ b/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -0,0 +1,62 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; +import { Server, ShieldCheck } from "lucide-react"; +import { AddRegistry } from "./add-docker-registry"; +import { AddSelfRegistry } from "./add-self-registry"; + +export const ShowRegistry = () => { + const { data } = api.certificates.all.useQuery(); + + return ( +
+ + + Clusters + Add cluster to your application. + + + {data?.length === 0 ? ( +
+ + + To create a cluster is required to set a registry. + + +
+ + +
+ + {/* */} +
+ ) : ( +
+ {data?.map((destination, index) => ( +
+ + {index + 1}. {destination.name} + +
+ {/* */} +
+
+ ))} +
{/* */}
+
+ )} +
+
+
+ ); +}; diff --git a/components/layouts/settings-layout.tsx b/components/layouts/settings-layout.tsx index 094bf40ea..8371814e3 100644 --- a/components/layouts/settings-layout.tsx +++ b/components/layouts/settings-layout.tsx @@ -59,6 +59,12 @@ export const SettingsLayout = ({ children }: Props) => { icon: Users, href: "/dashboard/settings/users", }, + { + title: "Cluster", + label: "", + icon: Server, + href: "/dashboard/settings/cluster", + }, ] : []), ]} @@ -75,6 +81,7 @@ import { Activity, Database, Route, + Server, ShieldCheck, User2, Users, diff --git a/pages/dashboard/settings/cluster.tsx b/pages/dashboard/settings/cluster.tsx new file mode 100644 index 000000000..98ce5cfe9 --- /dev/null +++ b/pages/dashboard/settings/cluster.tsx @@ -0,0 +1,42 @@ +import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates"; +import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { SettingsLayout } from "@/components/layouts/settings-layout"; +import { validateRequest } from "@/server/auth/auth"; +import type { GetServerSidePropsContext } from "next"; +import React, { type ReactElement } from "react"; + +const Page = () => { + return ( +
+ +
+ ); +}; + +export default Page; + +Page.getLayout = (page: ReactElement) => { + return ( + + {page} + + ); +}; +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }>, +) { + const { user, session } = await validateRequest(ctx.req, ctx.res); + if (!user || user.rol === "user") { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + + return { + props: {}, + }; +} diff --git a/server/api/routers/registry.ts b/server/api/routers/registry.ts new file mode 100644 index 000000000..725861c5f --- /dev/null +++ b/server/api/routers/registry.ts @@ -0,0 +1,66 @@ +import { + apiCreateRegistry, + apiEnableSelfHostedRegistry, + apiFindOneRegistry, + apiRemoveRegistry, + apiUpdateRegistry, +} from "@/server/db/schema"; +import { + createRegistry, + findRegistryById, + removeRegistry, + updaterRegistry, +} from "../services/registry"; +import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc"; +import { TRPCError } from "@trpc/server"; + +export const registryRouter = createTRPCRouter({ + create: adminProcedure + .input(apiCreateRegistry) + .mutation(async ({ ctx, input }) => { + return await createRegistry(input); + }), + remove: adminProcedure + .input(apiRemoveRegistry) + .mutation(async ({ ctx, input }) => { + return await removeRegistry(input.registryId); + }), + update: protectedProcedure + .input(apiUpdateRegistry) + .mutation(async ({ input }) => { + const { registryId, ...rest } = input; + const application = await updaterRegistry(registryId, { + ...rest, + }); + + if (!application) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update registry", + }); + } + + return true; + }), + findOne: adminProcedure.input(apiFindOneRegistry).query(async ({ input }) => { + return await findRegistryById(input.registryId); + }), + + enableSelfHostedRegistry: protectedProcedure + .input(apiEnableSelfHostedRegistry) + .mutation(async ({ input }) => { + // return await createRegistry({ + // username:"CUSTOM" + // adminId: input.adminId, + // }); + // const application = await findRegistryById(input.registryId); + // const result = await db + // .update(registry) + // .set({ + // selfHosted: true, + // }) + // .where(eq(registry.registryId, input.registryId)) + // .returning(); + // return result[0]; + }), +}); diff --git a/server/api/services/registry.ts b/server/api/services/registry.ts new file mode 100644 index 000000000..ac8f2080a --- /dev/null +++ b/server/api/services/registry.ts @@ -0,0 +1,84 @@ +import { type apiCreateRegistry, registry } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { db } from "@/server/db"; +import { eq } from "drizzle-orm"; + +export type Registry = typeof registry.$inferSelect; + +export const createRegistry = async (input: typeof apiCreateRegistry._type) => { + const newRegistry = await db + .insert(registry) + .values({ + ...input, + }) + .returning() + .then((value) => value[0]); + + if (!newRegistry) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting registry", + }); + } + return newRegistry; +}; + +export const removeRegistry = async (registryId: string) => { + try { + const response = await db + .delete(registry) + .where(eq(registry.registryId, registryId)) + .returning() + .then((res) => res[0]); + + if (!response) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Registry not found", + }); + } + + return response; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to remove this registry", + cause: error, + }); + } +}; + +export const updaterRegistry = async ( + registryId: string, + registryData: Partial, +) => { + try { + const response = await db + .update(registry) + .set({ + ...registryData, + }) + .where(eq(registry.registryId, registryId)) + .returning(); + + return response[0]; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update this registry", + }); + } +}; + +export const findRegistryById = async (registryId: string) => { + const registryResponse = await db.query.registry.findFirst({ + where: eq(registry.registryId, registryId), + }); + if (!registryResponse) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Registry not found", + }); + } + return registryResponse; +}; diff --git a/server/db/schema/admin.ts b/server/db/schema/admin.ts index 83306d2f4..5ce3317e1 100644 --- a/server/db/schema/admin.ts +++ b/server/db/schema/admin.ts @@ -6,6 +6,7 @@ import { users } from "./user"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod"; import { certificateType } from "./shared"; +import { registry } from "./registry"; export const admins = pgTable("admin", { adminId: text("adminId") @@ -39,6 +40,7 @@ export const adminsRelations = relations(admins, ({ one, many }) => ({ references: [auth.id], }), users: many(users), + registry: many(registry), })); const createSchema = createInsertSchema(admins, { diff --git a/server/db/schema/index.ts b/server/db/schema/index.ts index fc1d412db..3fe43e1ae 100644 --- a/server/db/schema/index.ts +++ b/server/db/schema/index.ts @@ -1,6 +1,5 @@ export * from "./application"; export * from "./postgres"; - export * from "./user"; export * from "./admin"; export * from "./auth"; @@ -20,3 +19,4 @@ export * from "./security"; export * from "./port"; export * from "./redis"; export * from "./shared"; +export * from "./registry"; diff --git a/server/db/schema/registry.ts b/server/db/schema/registry.ts new file mode 100644 index 000000000..b502648a6 --- /dev/null +++ b/server/db/schema/registry.ts @@ -0,0 +1,87 @@ +import { createInsertSchema } from "drizzle-zod"; +import { nanoid } from "nanoid"; +import { relations, sql } from "drizzle-orm"; +import { boolean, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { auth } from "./auth"; +import { admins } from "./admin"; +import { z } from "zod"; +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const registryType = pgEnum("RegistryType", ["selfHosted", "cloud"]); + +export const registry = pgTable("registry", { + registryId: text("registryId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + registryName: text("registryName").notNull(), + username: text("username").notNull(), + password: text("password").notNull(), + registryUrl: text("registryUrl").notNull(), + createdAt: text("createdAt") + .notNull() + .$defaultFn(() => new Date().toISOString()), + registryType: registryType("selfHosted").notNull().default("cloud"), + adminId: text("adminId") + .notNull() + .references(() => admins.adminId, { onDelete: "cascade" }), +}); + +export const registryRelations = relations(registry, ({ one }) => ({ + admin: one(admins, { + fields: [registry.adminId], + references: [admins.adminId], + }), +})); + +const createSchema = createInsertSchema(registry, { + registryName: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + registryUrl: z.string().min(1), + adminId: z.string().min(1), + registryId: z.string().min(1), +}); + +export const apiCreateRegistry = createSchema + .pick({}) + .extend({ + registryName: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + registryUrl: z.string().min(1), + adminId: z.string().min(1), + }) + .required(); + +export const apiRemoveRegistry = createSchema + .pick({ + registryId: true, + }) + .required(); + +export const apiFindOneRegistry = createSchema + .pick({ + registryId: true, + }) + .required(); + +export const apiUpdateRegistry = createSchema + .pick({ + password: true, + registryName: true, + username: true, + registryUrl: true, + registryId: true, + }) + .required(); + +export const apiEnableSelfHostedRegistry = createSchema + .pick({ + adminId: true, + }) + .required();