diff --git a/components/dashboard/project/add-application.tsx b/components/dashboard/project/add-application.tsx index 7edc10b8..3eb66b1a 100644 --- a/components/dashboard/project/add-application.tsx +++ b/components/dashboard/project/add-application.tsx @@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { AlertTriangle, Folder } from "lucide-react"; -import { useEffect } from "react"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; @@ -31,6 +31,15 @@ const AddTemplateSchema = z.object({ name: z.string().min(1, { message: "Name is required", }), + appName: z + .string() + .min(1, { + message: "App name is required", + }) + .regex(/^[a-z](?!.*--)([a-z0-9-]*[a-z])?$/, { + message: + "App name supports letters, numbers, '-' and can only start and end letters, and does not support continuous '-'", + }), description: z.string().optional(), }); @@ -38,10 +47,12 @@ type AddTemplate = z.infer; interface Props { projectId: string; + projectName?: string; } -export const AddApplication = ({ projectId }: Props) => { +export const AddApplication = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); + const [visible, setVisible] = useState(false); const { mutateAsync, isLoading, error, isError } = api.application.create.useMutation(); @@ -49,34 +60,34 @@ export const AddApplication = ({ projectId }: Props) => { const form = useForm({ defaultValues: { name: "", + appName: `${projectName}-`, description: "", }, resolver: zodResolver(AddTemplateSchema), }); - useEffect(() => { - form.reset(); - }, [form, form.reset, form.formState.isSubmitSuccessful]); - const onSubmit = async (data: AddTemplate) => { await mutateAsync({ name: data.name, + appName: data.appName, description: data.description, projectId, }) .then(async () => { toast.success("Service Created"); + form.reset(); + setVisible(false); await utils.project.one.invalidate({ projectId, }); }) - .catch(() => { + .catch((e) => { toast.error("Error to create the service"); }); }; return ( - + { onSubmit={form.handleSubmit(onSubmit)} className="grid w-full gap-4" > -
- ( - - Name - - - - - - - )} - /> -
+ ( + + Name + + { + const val = e.target.value?.trim() || ""; + form.setValue("appName", `${projectName}-${val}`); + field.onChange(val); + }} + /> + + + + )} + /> + ( + + AppName + + + + + + )} + /> , + label: "PostgreSQL", + }, + mongo: { + icon: , + label: "MongoDB", + }, + mariadb: { + icon: , + label: "MariaDB", + }, + mysql: { + icon: , + label: "MySQL", + }, + redis: { + icon: , + label: "Redis", + }, +}; + type AddDatabase = z.infer; interface Props { projectId: string; + projectName?: string; } -export const AddDatabase = ({ projectId }: Props) => { +export const AddDatabase = ({ projectId, projectName }: Props) => { const utils = api.useUtils(); + const [visible, setVisible] = useState(false); - const { mutateAsync: createPostgresql } = api.postgres.create.useMutation(); - - const { mutateAsync: createMongo } = api.mongo.create.useMutation(); - - const { mutateAsync: createRedis } = api.redis.create.useMutation(); - - const { mutateAsync: createMariadb } = api.mariadb.create.useMutation(); - - const { mutateAsync: createMysql } = api.mysql.create.useMutation(); + const postgresMutation = api.postgres.create.useMutation(); + const mongoMutation = api.mongo.create.useMutation(); + const redisMutation = api.redis.create.useMutation(); + const mariadbMutation = api.mariadb.create.useMutation(); + const mysqlMutation = api.mysql.create.useMutation(); const form = useForm({ defaultValues: { type: "postgres", dockerImage: "", name: "", + appName: `${projectName}-`, databasePassword: "", description: "", databaseName: "", @@ -133,76 +164,65 @@ export const AddDatabase = ({ projectId }: Props) => { resolver: zodResolver(mySchema), }); const type = form.watch("type"); - - useEffect(() => { - form.reset({ - type: "postgres", - dockerImage: "", - name: "", - databasePassword: "", - description: "", - databaseName: "", - databaseUser: "", - }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); + const activeMutation = { + postgres: postgresMutation, + mongo: mongoMutation, + redis: redisMutation, + mariadb: mariadbMutation, + mysql: mysqlMutation, + }; const onSubmit = async (data: AddDatabase) => { const defaultDockerImage = data.dockerImage || dockerImageDefaultPlaceholder[data.type]; let promise: Promise | null = null; + const commonParams = { + name: data.name, + appName: data.appName, + dockerImage: defaultDockerImage, + projectId, + description: data.description, + }; + if (data.type === "postgres") { - promise = createPostgresql({ - name: data.name, - dockerImage: defaultDockerImage, + promise = postgresMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - projectId, - description: data.description, }); } else if (data.type === "mongo") { - promise = createMongo({ - name: data.name, - dockerImage: defaultDockerImage, + promise = mongoMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - projectId, - description: data.description, }); } else if (data.type === "redis") { - promise = createRedis({ - name: data.name, - dockerImage: defaultDockerImage, + promise = redisMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, projectId, - description: data.description, }); } else if (data.type === "mariadb") { - promise = createMariadb({ - name: data.name, - dockerImage: defaultDockerImage, + promise = mariadbMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, - projectId, databaseRootPassword: data.databaseRootPassword, databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - description: data.description, }); } else if (data.type === "mysql") { - promise = createMysql({ - name: data.name, - dockerImage: defaultDockerImage, + promise = mysqlMutation.mutateAsync({ + ...commonParams, databasePassword: data.databasePassword, databaseName: data.databaseName, databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], - projectId, databaseRootPassword: data.databaseRootPassword, - description: data.description, }); } @@ -210,6 +230,17 @@ export const AddDatabase = ({ projectId }: Props) => { await promise .then(async () => { toast.success("Database Created"); + form.reset({ + type: "postgres", + dockerImage: "", + name: "", + appName: `${projectName}-`, + databasePassword: "", + description: "", + databaseName: "", + databaseUser: "", + }); + setVisible(false); await utils.project.one.invalidate({ projectId, }); @@ -220,7 +251,7 @@ export const AddDatabase = ({ projectId }: Props) => { } }; return ( - + { Database - + Databases - {/* {isError && ( -
- - - {error?.message} - -
- )} */}
{ defaultValue={field.value} className="grid w-full grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" > - - -
- - -
-
-
- - -
- - -
-
-
- - -
- - -
-
-
- - -
- - -
-
-
- - -
- - -
-
-
+ {Object.entries(databasesMap).map(([key, value]) => ( + + +
+ + +
+
+
+ ))} + {activeMutation[field.value].isError && ( +
+ + + {activeMutation[field.value].error?.message} + +
+ )} )} /> @@ -372,13 +336,34 @@ export const AddDatabase = ({ projectId }: Props) => { Name - + { + const val = e.target.value?.trim() || ""; + form.setValue("appName", `${projectName}-${val}`); + field.onChange(val); + }} + /> )} /> + ( + + AppName + + + + + + )} + /> - - + + )} diff --git a/server/api/routers/application.ts b/server/api/routers/application.ts index 304d0449..dd23d2aa 100644 --- a/server/api/routers/application.ts +++ b/server/api/routers/application.ts @@ -65,7 +65,10 @@ export const applicationRouter = createTRPCRouter({ if (ctx.user.rol === "user") { await addNewService(ctx.user.authId, newApplication.applicationId); } - } catch (error) { + } catch (error: unknown) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error to create the application", diff --git a/server/api/routers/mariadb.ts b/server/api/routers/mariadb.ts index 59e57748..2ab8dd6a 100644 --- a/server/api/routers/mariadb.ts +++ b/server/api/routers/mariadb.ts @@ -49,6 +49,9 @@ export const mariadbRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting mariadb database", diff --git a/server/api/routers/mongo.ts b/server/api/routers/mongo.ts index 705549b6..d9ddd2c2 100644 --- a/server/api/routers/mongo.ts +++ b/server/api/routers/mongo.ts @@ -49,6 +49,9 @@ export const mongoRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting mongo database", diff --git a/server/api/routers/mysql.ts b/server/api/routers/mysql.ts index 02f683ba..f520064b 100644 --- a/server/api/routers/mysql.ts +++ b/server/api/routers/mysql.ts @@ -50,6 +50,9 @@ export const mysqlRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting mysql database", diff --git a/server/api/routers/postgres.ts b/server/api/routers/postgres.ts index 45bd88a1..4dc7ff5d 100644 --- a/server/api/routers/postgres.ts +++ b/server/api/routers/postgres.ts @@ -49,6 +49,9 @@ export const postgresRouter = createTRPCRouter({ return true; } catch (error) { + if (error instanceof TRPCError) { + throw error; + } throw new TRPCError({ code: "BAD_REQUEST", message: "Error input: Inserting postgresql database", diff --git a/server/api/services/application.ts b/server/api/services/application.ts index d5da79c4..0a002ea1 100644 --- a/server/api/services/application.ts +++ b/server/api/services/application.ts @@ -15,11 +15,23 @@ import { findAdmin } from "./admin"; import { createTraefikConfig } from "@/server/utils/traefik/application"; import { docker } from "@/server/constants"; import { getAdvancedStats } from "@/server/monitoring/utilts"; +import { validUniqueServerAppName } from "./project"; export type Application = typeof applications.$inferSelect; export const createApplication = async ( input: typeof apiCreateApplication._type, ) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Application with this 'AppName' already exists", + }); + } + } + return await db.transaction(async (tx) => { const newApplication = await tx .insert(applications) diff --git a/server/api/services/mariadb.ts b/server/api/services/mariadb.ts index 7545087f..1ebd3525 100644 --- a/server/api/services/mariadb.ts +++ b/server/api/services/mariadb.ts @@ -5,10 +5,22 @@ import { buildMariadb } from "@/server/utils/databases/mariadb"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Mariadb = typeof mariadb.$inferSelect; export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newMariadb = await db .insert(mariadb) .values({ diff --git a/server/api/services/mongo.ts b/server/api/services/mongo.ts index a7605ffe..e6114ef4 100644 --- a/server/api/services/mongo.ts +++ b/server/api/services/mongo.ts @@ -5,10 +5,22 @@ import { buildMongo } from "@/server/utils/databases/mongo"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Mongo = typeof mongo.$inferSelect; export const createMongo = async (input: typeof apiCreateMongo._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newMongo = await db .insert(mongo) .values({ diff --git a/server/api/services/mysql.ts b/server/api/services/mysql.ts index b09aadaa..3482968d 100644 --- a/server/api/services/mysql.ts +++ b/server/api/services/mysql.ts @@ -5,11 +5,22 @@ import { buildMysql } from "@/server/utils/databases/mysql"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; -import { nanoid } from "nanoid"; +import { validUniqueServerAppName } from "./project"; export type MySql = typeof mysql.$inferSelect; export const createMysql = async (input: typeof apiCreateMySql._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newMysql = await db .insert(mysql) .values({ diff --git a/server/api/services/postgres.ts b/server/api/services/postgres.ts index 9575ac51..11ac1085 100644 --- a/server/api/services/postgres.ts +++ b/server/api/services/postgres.ts @@ -5,10 +5,22 @@ import { buildPostgres } from "@/server/utils/databases/postgres"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Postgres = typeof postgres.$inferSelect; export const createPostgres = async (input: typeof apiCreatePostgres._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newPostgres = await db .insert(postgres) .values({ diff --git a/server/api/services/project.ts b/server/api/services/project.ts index 687c67f5..df75c5c9 100644 --- a/server/api/services/project.ts +++ b/server/api/services/project.ts @@ -1,5 +1,14 @@ import { db } from "@/server/db"; -import { type apiCreateProject, projects } from "@/server/db/schema"; +import { + type apiCreateProject, + applications, + mariadb, + mongo, + mysql, + postgres, + projects, + redis, +} from "@/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { findAdmin } from "./admin"; @@ -73,3 +82,41 @@ export const updateProjectById = async ( return result; }; + +export const validUniqueServerAppName = async (appName: string) => { + const query = await db.query.projects.findMany({ + with: { + applications: { + where: eq(applications.appName, appName), + }, + mariadb: { + where: eq(mariadb.appName, appName), + }, + mongo: { + where: eq(mongo.appName, appName), + }, + mysql: { + where: eq(mysql.appName, appName), + }, + postgres: { + where: eq(postgres.appName, appName), + }, + redis: { + where: eq(redis.appName, appName), + }, + }, + }); + + // Filter out items with non-empty fields + const nonEmptyProjects = query.filter( + (project) => + project.applications.length > 0 || + project.mariadb.length > 0 || + project.mongo.length > 0 || + project.mysql.length > 0 || + project.postgres.length > 0 || + project.redis.length > 0, + ); + + return nonEmptyProjects.length === 0; +}; diff --git a/server/api/services/redis.ts b/server/api/services/redis.ts index e04bf41b..6137b922 100644 --- a/server/api/services/redis.ts +++ b/server/api/services/redis.ts @@ -5,11 +5,23 @@ import { buildRedis } from "@/server/utils/databases/redis"; import { pullImage } from "@/server/utils/docker/utils"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; export type Redis = typeof redis.$inferSelect; // https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881 export const createRedis = async (input: typeof apiCreateRedis._type) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newRedis = await db .insert(redis) .values({ diff --git a/server/db/schema/application.ts b/server/db/schema/application.ts index abdeed53..2c33907b 100644 --- a/server/db/schema/application.ts +++ b/server/db/schema/application.ts @@ -128,6 +128,7 @@ const createSchema = createInsertSchema(applications, { export const apiCreateApplication = createSchema.pick({ name: true, + appName: true, description: true, projectId: true, }); diff --git a/server/db/schema/mariadb.ts b/server/db/schema/mariadb.ts index 256dfbfb..96e65a71 100644 --- a/server/db/schema/mariadb.ts +++ b/server/db/schema/mariadb.ts @@ -79,6 +79,7 @@ const createSchema = createInsertSchema(mariadb, { export const apiCreateMariaDB = createSchema .pick({ name: true, + appName: true, dockerImage: true, databaseRootPassword: true, projectId: true, diff --git a/server/db/schema/mongo.ts b/server/db/schema/mongo.ts index 7406580e..bbd94c2c 100644 --- a/server/db/schema/mongo.ts +++ b/server/db/schema/mongo.ts @@ -73,6 +73,7 @@ const createSchema = createInsertSchema(mongo, { export const apiCreateMongo = createSchema .pick({ name: true, + appName: true, dockerImage: true, projectId: true, description: true, diff --git a/server/db/schema/mysql.ts b/server/db/schema/mysql.ts index 9e0c8c77..986ab88d 100644 --- a/server/db/schema/mysql.ts +++ b/server/db/schema/mysql.ts @@ -77,6 +77,7 @@ const createSchema = createInsertSchema(mysql, { export const apiCreateMySql = createSchema .pick({ name: true, + appName: true, dockerImage: true, projectId: true, description: true, diff --git a/server/db/schema/postgres.ts b/server/db/schema/postgres.ts index 7cf0f34d..9684f478 100644 --- a/server/db/schema/postgres.ts +++ b/server/db/schema/postgres.ts @@ -74,6 +74,7 @@ const createSchema = createInsertSchema(postgres, { export const apiCreatePostgres = createSchema .pick({ name: true, + appName: true, databaseName: true, databaseUser: true, databasePassword: true, diff --git a/server/db/schema/redis.ts b/server/db/schema/redis.ts index 842fe809..003bbdd2 100644 --- a/server/db/schema/redis.ts +++ b/server/db/schema/redis.ts @@ -69,6 +69,7 @@ const createSchema = createInsertSchema(redis, { export const apiCreateRedis = createSchema .pick({ name: true, + appName: true, databasePassword: true, dockerImage: true, projectId: true,