From a439286e5f7fa242aa4f21318db369d4006d50f0 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Fri, 13 Dec 2024 16:14:52 +0100 Subject: [PATCH 01/85] feat: add UI selection --- .../dashboard/compose/delete-compose.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/apps/dokploy/components/dashboard/compose/delete-compose.tsx b/apps/dokploy/components/dashboard/compose/delete-compose.tsx index 07f42448c..b0b958f21 100644 --- a/apps/dokploy/components/dashboard/compose/delete-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/delete-compose.tsx @@ -1,4 +1,5 @@ import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -11,6 +12,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -30,6 +32,7 @@ const deleteComposeSchema = z.object({ projectName: z.string().min(1, { message: "Compose name is required", }), + deleteVolumes: z.boolean(), }); type DeleteCompose = z.infer; @@ -49,6 +52,7 @@ export const DeleteCompose = ({ composeId }: Props) => { const form = useForm({ defaultValues: { projectName: "", + deleteVolumes: false, }, resolver: zodResolver(deleteComposeSchema), }); @@ -114,6 +118,27 @@ export const DeleteCompose = ({ composeId }: Props) => { )} /> + ( + +
+ + + + + + Delete volumes associated with this compose + +
+ +
+ )} + /> From 4dc7d9e3c81f47d0cac33853b5bd78b8f3875740 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Fri, 13 Dec 2024 16:33:54 +0100 Subject: [PATCH 02/85] feat: add params to mutation --- apps/dokploy/components/dashboard/compose/delete-compose.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/compose/delete-compose.tsx b/apps/dokploy/components/dashboard/compose/delete-compose.tsx index b0b958f21..7d5ab63b9 100644 --- a/apps/dokploy/components/dashboard/compose/delete-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/delete-compose.tsx @@ -60,7 +60,8 @@ export const DeleteCompose = ({ composeId }: Props) => { const onSubmit = async (formData: DeleteCompose) => { const expectedName = `${data?.name}/${data?.appName}`; if (formData.projectName === expectedName) { - await mutateAsync({ composeId }) + const { deleteVolumes } = formData; + await mutateAsync({ composeId, deleteVolumes }) .then((result) => { push(`/dashboard/project/${result?.projectId}`); toast.success("Compose deleted successfully"); From 8779c67b71935ae0426ade6cfb704b51db21ff33 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Fri, 13 Dec 2024 16:34:12 +0100 Subject: [PATCH 03/85] refactor: change import names --- apps/dokploy/server/api/routers/compose.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 6d04e815f..5f53752b5 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -3,6 +3,7 @@ import { db } from "@/server/db"; import { apiCreateCompose, apiCreateComposeByTemplate, + apiDeleteCompose, apiFetchServices, apiFindCompose, apiRandomizeCompose, @@ -117,7 +118,7 @@ export const composeRouter = createTRPCRouter({ return updateCompose(input.composeId, input); }), delete: protectedProcedure - .input(apiFindCompose) + .input(apiDeleteCompose) .mutation(async ({ input, ctx }) => { if (ctx.user.rol === "user") { await checkServiceAccess(ctx.user.authId, input.composeId, "delete"); @@ -138,7 +139,7 @@ export const composeRouter = createTRPCRouter({ .returning(); const cleanupOperations = [ - async () => await removeCompose(composeResult), + async () => await removeCompose(composeResult, input.deleteVolumes), async () => await removeDeploymentsByComposeId(composeResult), async () => await removeComposeDirectory(composeResult.appName), ]; From 3decbd52075fd2629ef774274a936925ba377eb2 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Fri, 13 Dec 2024 16:34:29 +0100 Subject: [PATCH 04/85] feat: add new form validator --- packages/server/src/db/schema/compose.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/server/src/db/schema/compose.ts b/packages/server/src/db/schema/compose.ts index 02bac7813..e0c4863b5 100644 --- a/packages/server/src/db/schema/compose.ts +++ b/packages/server/src/db/schema/compose.ts @@ -155,6 +155,11 @@ export const apiFindCompose = z.object({ composeId: z.string().min(1), }); +export const apiDeleteCompose = z.object({ + composeId: z.string().min(1), + deleteVolumes: z.boolean(), +}); + export const apiFetchServices = z.object({ composeId: z.string().min(1), type: z.enum(["fetch", "cache"]).optional().default("cache"), From 92c2a83d92bd74151ad73b921219c9cfd770aaa1 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Fri, 13 Dec 2024 16:34:46 +0100 Subject: [PATCH 05/85] feat: add filter to delete volumes is wanted --- packages/server/src/services/compose.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 5ae0d7740..c4b88c68f 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -436,13 +436,26 @@ export const rebuildRemoteCompose = async ({ return true; }; -export const removeCompose = async (compose: Compose) => { +export const removeCompose = async ( + compose: Compose, + deleteVolumes: boolean, +) => { try { const { COMPOSE_PATH } = paths(!!compose.serverId); const projectPath = join(COMPOSE_PATH, compose.appName); + console.log("API: DELETE VOLUMES=", deleteVolumes); + if (compose.composeType === "stack") { - const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; + const listVolumesCommand = `docker volume ls --format \"{{.Name}}\" | grep ${compose.appName}`; + const removeVolumesCommand = `${listVolumesCommand} | xargs -r docker volume rm`; + let command: string; + if (deleteVolumes) { + command = `cd ${projectPath} && docker stack rm ${compose.appName} && ${removeVolumesCommand} && rm -rf ${projectPath}`; + } else { + command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; + } + if (compose.serverId) { await execAsyncRemote(compose.serverId, command); } else { @@ -452,7 +465,13 @@ export const removeCompose = async (compose: Compose) => { cwd: projectPath, }); } else { - const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`; + let command: string; + if (deleteVolumes) { + command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`; + } else { + command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`; + } + if (compose.serverId) { await execAsyncRemote(compose.serverId, command); } else { From 8e5b0988cfb08b535e8463cc482e37a65559bba6 Mon Sep 17 00:00:00 2001 From: Aaron Gonzales Date: Mon, 16 Dec 2024 18:13:07 +0800 Subject: [PATCH 06/85] fix: fixed/improved handling of app names in api --- packages/server/src/db/schema/utils.ts | 15 ++++++++ packages/server/src/services/application.ts | 35 +++++++++---------- packages/server/src/services/compose.ts | 27 +++++++------- packages/server/src/services/deployment.ts | 2 +- packages/server/src/services/mariadb.ts | 23 ++++++------ packages/server/src/services/mongo.ts | 23 ++++++------ packages/server/src/services/mysql.ts | 22 ++++++------ packages/server/src/services/postgres.ts | 21 ++++++----- .../server/src/services/preview-deployment.ts | 16 ++++----- packages/server/src/services/redis.ts | 21 ++++++----- 10 files changed, 107 insertions(+), 98 deletions(-) diff --git a/packages/server/src/db/schema/utils.ts b/packages/server/src/db/schema/utils.ts index 59ebf4b7d..e86517697 100644 --- a/packages/server/src/db/schema/utils.ts +++ b/packages/server/src/db/schema/utils.ts @@ -1,3 +1,4 @@ +import { generatePassword } from "@dokploy/server/templates/utils"; import { faker } from "@faker-js/faker"; import { customAlphabet } from "nanoid"; @@ -13,3 +14,17 @@ export const generateAppName = (type: string) => { const nanoidPart = customNanoid(); return `${type}-${randomFakerElement}-${nanoidPart}`; }; + +export const cleanAppName = (appName?: string) => { + if (!appName) { + return appName; + } + return appName.trim().replace(/ /g, "-"); +}; + +export const buildAppName = (type: string, baseAppName?: string) => { + if (baseAppName) { + return `${cleanAppName(baseAppName)}-${generatePassword(6)}`; + } + return generateAppName(type); +}; diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index fef1457c0..d2ce34320 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -3,10 +3,10 @@ import { db } from "@dokploy/server/db"; import { type apiCreateApplication, applications, + buildAppName, + cleanAppName, } from "@dokploy/server/db/schema"; -import { generateAppName } from "@dokploy/server/db/schema"; import { getAdvancedStats } from "@dokploy/server/monitoring/utilts"; -import { generatePassword } from "@dokploy/server/templates/utils"; import { buildApplication, getBuildCommand, @@ -46,34 +46,31 @@ import { createDeploymentPreview, updateDeploymentStatus, } from "./deployment"; -import { validUniqueServerAppName } from "./project"; -import { - findPreviewDeploymentById, - updatePreviewDeployment, -} from "./preview-deployment"; +import { type Domain, getDomainHost } from "./domain"; import { createPreviewDeploymentComment, getIssueComment, issueCommentExists, updateIssueComment, } from "./github"; -import { type Domain, getDomainHost } from "./domain"; +import { + findPreviewDeploymentById, + updatePreviewDeployment, +} from "./preview-deployment"; +import { validUniqueServerAppName } from "./project"; export type Application = typeof applications.$inferSelect; export const createApplication = async ( input: typeof apiCreateApplication._type, ) => { - input.appName = - `${input.appName}-${generatePassword(6)}` || generateAppName("app"); - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); + const appName = buildAppName("app", input.appName); - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Application with this 'AppName' already exists", - }); - } + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Application with this 'AppName' already exists", + }); } return await db.transaction(async (tx) => { @@ -81,6 +78,7 @@ export const createApplication = async ( .insert(applications) .values({ ...input, + appName, }) .returning() .then((value) => value[0]); @@ -144,6 +142,7 @@ export const updateApplication = async ( .update(applications) .set({ ...applicationData, + appName: cleanAppName(applicationData.appName), }) .where(eq(applications.applicationId, applicationId)) .returning(); diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 5ae0d7740..27e7bfa47 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -2,7 +2,7 @@ import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import { db } from "@dokploy/server/db"; import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; -import { generateAppName } from "@dokploy/server/db/schema"; +import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildCompose, @@ -52,17 +52,14 @@ import { validUniqueServerAppName } from "./project"; export type Compose = typeof compose.$inferSelect; export const createCompose = async (input: typeof apiCreateCompose._type) => { - input.appName = - `${input.appName}-${generatePassword(6)}` || generateAppName("compose"); - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); + const appName = buildAppName("compose", input.appName); - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); } const newDestination = await db @@ -70,6 +67,7 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => { .values({ ...input, composeFile: "", + appName, }) .returning() .then((value) => value[0]); @@ -87,8 +85,9 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => { export const createComposeByTemplate = async ( input: typeof compose.$inferInsert, ) => { - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); + const appName = cleanAppName(input.appName); + if (appName) { + const valid = await validUniqueServerAppName(appName); if (!valid) { throw new TRPCError({ @@ -101,6 +100,7 @@ export const createComposeByTemplate = async ( .insert(compose) .values({ ...input, + appName, }) .returning() .then((value) => value[0]); @@ -188,6 +188,7 @@ export const updateCompose = async ( .update(compose) .set({ ...composeData, + appName: cleanAppName(composeData.appName), }) .where(eq(compose.composeId, composeId)) .returning(); diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index b18b132de..41adf2380 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -23,8 +23,8 @@ import { type Server, findServerById } from "./server"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { - findPreviewDeploymentById, type PreviewDeployment, + findPreviewDeploymentById, updatePreviewDeployment, } from "./preview-deployment"; diff --git a/packages/server/src/services/mariadb.ts b/packages/server/src/services/mariadb.ts index 645b5c657..7906cc5b7 100644 --- a/packages/server/src/services/mariadb.ts +++ b/packages/server/src/services/mariadb.ts @@ -4,7 +4,7 @@ import { backups, mariadb, } from "@dokploy/server/db/schema"; -import { generateAppName } from "@dokploy/server/db/schema"; +import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildMariadb } from "@dokploy/server/utils/databases/mariadb"; import { pullImage } from "@dokploy/server/utils/docker/utils"; @@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; export type Mariadb = typeof mariadb.$inferSelect; export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { - input.appName = - `${input.appName}-${generatePassword(6)}` || generateAppName("mariadb"); - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); - - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } + const appName = buildAppName("mariadb", 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 @@ -40,6 +37,7 @@ export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { databaseRootPassword: input.databaseRootPassword ? input.databaseRootPassword : generatePassword(), + appName, }) .returning() .then((value) => value[0]); @@ -86,6 +84,7 @@ export const updateMariadbById = async ( .update(mariadb) .set({ ...mariadbData, + appName: cleanAppName(mariadbData.appName), }) .where(eq(mariadb.mariadbId, mariadbId)) .returning(); diff --git a/packages/server/src/services/mongo.ts b/packages/server/src/services/mongo.ts index b87ec4da9..82a8a84f6 100644 --- a/packages/server/src/services/mongo.ts +++ b/packages/server/src/services/mongo.ts @@ -1,6 +1,6 @@ import { db } from "@dokploy/server/db"; import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema"; -import { generateAppName } from "@dokploy/server/db/schema"; +import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildMongo } from "@dokploy/server/utils/databases/mongo"; import { pullImage } from "@dokploy/server/utils/docker/utils"; @@ -13,17 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; export type Mongo = typeof mongo.$inferSelect; export const createMongo = async (input: typeof apiCreateMongo._type) => { - input.appName = - `${input.appName}-${generatePassword(6)}` || generateAppName("mongo"); - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); - - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } + const appName = buildAppName("mongo", input.appName); + + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); } const newMongo = await db @@ -33,6 +30,7 @@ export const createMongo = async (input: typeof apiCreateMongo._type) => { databasePassword: input.databasePassword ? input.databasePassword : generatePassword(), + appName, }) .returning() .then((value) => value[0]); @@ -78,6 +76,7 @@ export const updateMongoById = async ( .update(mongo) .set({ ...mongoData, + appName: cleanAppName(mongoData.appName), }) .where(eq(mongo.mongoId, mongoId)) .returning(); diff --git a/packages/server/src/services/mysql.ts b/packages/server/src/services/mysql.ts index ee9df8206..1bb2c4787 100644 --- a/packages/server/src/services/mysql.ts +++ b/packages/server/src/services/mysql.ts @@ -1,6 +1,6 @@ import { db } from "@dokploy/server/db"; import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema"; -import { generateAppName } from "@dokploy/server/db/schema"; +import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildMysql } from "@dokploy/server/utils/databases/mysql"; import { pullImage } from "@dokploy/server/utils/docker/utils"; @@ -13,18 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; export type MySql = typeof mysql.$inferSelect; export const createMysql = async (input: typeof apiCreateMySql._type) => { - input.appName = - `${input.appName}-${generatePassword(6)}` || generateAppName("mysql"); + const appName = buildAppName("mysql", input.appName); - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); - - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); } const newMysql = await db @@ -37,6 +33,7 @@ export const createMysql = async (input: typeof apiCreateMySql._type) => { databaseRootPassword: input.databaseRootPassword ? input.databaseRootPassword : generatePassword(), + appName, }) .returning() .then((value) => value[0]); @@ -83,6 +80,7 @@ export const updateMySqlById = async ( .update(mysql) .set({ ...mysqlData, + appName: cleanAppName(mysqlData.appName), }) .where(eq(mysql.mysqlId, mysqlId)) .returning(); diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts index c94ddbbe2..d2cd4874b 100644 --- a/packages/server/src/services/postgres.ts +++ b/packages/server/src/services/postgres.ts @@ -4,7 +4,7 @@ import { backups, postgres, } from "@dokploy/server/db/schema"; -import { generateAppName } from "@dokploy/server/db/schema"; +import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildPostgres } from "@dokploy/server/utils/databases/postgres"; import { pullImage } from "@dokploy/server/utils/docker/utils"; @@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; export type Postgres = typeof postgres.$inferSelect; export const createPostgres = async (input: typeof apiCreatePostgres._type) => { - input.appName = - `${input.appName}-${generatePassword(6)}` || generateAppName("postgres"); - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); + const appName = buildAppName("postgres", input.appName); - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); } const newPostgres = await db @@ -37,6 +34,7 @@ export const createPostgres = async (input: typeof apiCreatePostgres._type) => { databasePassword: input.databasePassword ? input.databasePassword : generatePassword(), + appName, }) .returning() .then((value) => value[0]); @@ -100,6 +98,7 @@ export const updatePostgresById = async ( .update(postgres) .set({ ...postgresData, + appName: cleanAppName(postgresData.appName), }) .where(eq(postgres.postgresId, postgresId)) .returning(); diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts index e52f45530..06bf8fb57 100644 --- a/packages/server/src/services/preview-deployment.ts +++ b/packages/server/src/services/preview-deployment.ts @@ -7,20 +7,20 @@ import { import { TRPCError } from "@trpc/server"; import { and, desc, eq } from "drizzle-orm"; import { slugify } from "../setup/server-setup"; -import { findApplicationById } from "./application"; -import { createDomain } from "./domain"; import { generatePassword, generateRandomDomain } from "../templates/utils"; +import { removeService } from "../utils/docker/utils"; +import { removeDirectoryCode } from "../utils/filesystem/directory"; +import { authGithub } from "../utils/providers/github"; +import { removeTraefikConfig } from "../utils/traefik/application"; import { manageDomain } from "../utils/traefik/domain"; +import { findAdminById } from "./admin"; +import { findApplicationById } from "./application"; import { removeDeployments, removeDeploymentsByPreviewDeploymentId, } from "./deployment"; -import { removeDirectoryCode } from "../utils/filesystem/directory"; -import { removeTraefikConfig } from "../utils/traefik/application"; -import { removeService } from "../utils/docker/utils"; -import { authGithub } from "../utils/providers/github"; -import { getIssueComment, type Github } from "./github"; -import { findAdminById } from "./admin"; +import { createDomain } from "./domain"; +import { type Github, getIssueComment } from "./github"; export type PreviewDeployment = typeof previewDeployments.$inferSelect; diff --git a/packages/server/src/services/redis.ts b/packages/server/src/services/redis.ts index 7809de288..96b3066c3 100644 --- a/packages/server/src/services/redis.ts +++ b/packages/server/src/services/redis.ts @@ -1,6 +1,6 @@ import { db } from "@dokploy/server/db"; import { type apiCreateRedis, redis } from "@dokploy/server/db/schema"; -import { generateAppName } from "@dokploy/server/db/schema"; +import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { buildRedis } from "@dokploy/server/utils/databases/redis"; import { pullImage } from "@dokploy/server/utils/docker/utils"; @@ -14,17 +14,14 @@ 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) => { - input.appName = - `${input.appName}-${generatePassword(6)}` || generateAppName("redis"); - if (input.appName) { - const valid = await validUniqueServerAppName(input.appName); + const appName = buildAppName("redis", input.appName); - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); } const newRedis = await db @@ -34,6 +31,7 @@ export const createRedis = async (input: typeof apiCreateRedis._type) => { databasePassword: input.databasePassword ? input.databasePassword : generatePassword(), + appName, }) .returning() .then((value) => value[0]); @@ -74,6 +72,7 @@ export const updateRedisById = async ( .update(redis) .set({ ...redisData, + appName: cleanAppName(redisData.appName), }) .where(eq(redis.redisId, redisId)) .returning(); From b592a025e48fc3a3df48bc1b06675c6a28a4d8a2 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:04:49 +0100 Subject: [PATCH 07/85] feat: add swarm router and related Docker service functions --- apps/dokploy/server/api/root.ts | 4 +- apps/dokploy/server/api/routers/swarm.ts | 31 +++++++ packages/server/src/services/docker.ts | 103 +++++++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/server/api/routers/swarm.ts diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index 85eb9763e..68f5e4e0c 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -21,6 +21,7 @@ import { mysqlRouter } from "./routers/mysql"; import { notificationRouter } from "./routers/notification"; import { portRouter } from "./routers/port"; import { postgresRouter } from "./routers/postgres"; +import { previewDeploymentRouter } from "./routers/preview-deployment"; import { projectRouter } from "./routers/project"; import { redirectsRouter } from "./routers/redirects"; import { redisRouter } from "./routers/redis"; @@ -30,8 +31,8 @@ import { serverRouter } from "./routers/server"; import { settingsRouter } from "./routers/settings"; import { sshRouter } from "./routers/ssh-key"; import { stripeRouter } from "./routers/stripe"; +import { swarmRouter } from "./routers/swarm"; import { userRouter } from "./routers/user"; -import { previewDeploymentRouter } from "./routers/preview-deployment"; /** * This is the primary router for your server. @@ -73,6 +74,7 @@ export const appRouter = createTRPCRouter({ github: githubRouter, server: serverRouter, stripe: stripeRouter, + swarm: swarmRouter, }); // export type definition of API diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts new file mode 100644 index 000000000..fe15d0ef3 --- /dev/null +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -0,0 +1,31 @@ +import { + getApplicationInfo, + getNodeApplications, + getNodeInfo, + getSwarmNodes, +} from "@dokploy/server"; +import { z } from "zod"; +import { createTRPCRouter, protectedProcedure } from "../trpc"; + +export const swarmRouter = createTRPCRouter({ + getNodes: protectedProcedure.query(async () => { + return await getSwarmNodes(); + }), + getNodeInfo: protectedProcedure + .input(z.object({ nodeId: z.string() })) + .query(async ({ input }) => { + return await getNodeInfo(input.nodeId); + }), + getNodeApps: protectedProcedure.query(async () => { + return getNodeApplications(); + }), + getAppInfos: protectedProcedure + .input( + z.object({ + appName: z.string(), + }), + ) + .query(async ({ input }) => { + return await getApplicationInfo(input.appName); + }), +}); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 6ac613542..8681cb228 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -224,3 +224,106 @@ export const containerRestart = async (containerId: string) => { return config; } catch (error) {} }; + +export const getSwarmNodes = async () => { + try { + const { stdout, stderr } = await execAsync( + "docker node ls --format '{{json .}}'", + ); + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + const nodes = JSON.parse(stdout); + + const nodesArray = stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + return nodesArray; + } catch (error) {} +}; + +export const getNodeInfo = async (nodeId: string) => { + try { + const { stdout, stderr } = await execAsync( + `docker node inspect ${nodeId} --format '{{json .}}'`, + ); + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + const nodeInfo = JSON.parse(stdout); + + return nodeInfo; + } catch (error) {} +}; + +export const getNodeApplications = async () => { + try { + // TODO: Implement this + // const { stdout, stderr } = await execAsync( + // `docker service ls --format '{{json .}}'` + // ); + + const stdout = `{"ID":"pxvnj68dxom9","Image":"dokploy/dokploy:latest","Mode":"replicated","Name":"dokploy","Ports":"","Replicas":"1/1"} +{"ID":"1sweo6dr2vrn","Image":"postgres:16","Mode":"replicated","Name":"dokploy-postgres","Ports":"","Replicas":"1/1"} +{"ID":"tnl2fck3rbop","Image":"redis:7","Mode":"replicated","Name":"dokploy-redis","Ports":"","Replicas":"1/1"} +{"ID":"o9ady4y1p96x","Image":"traefik:v3.1.2","Mode":"replicated","Name":"dokploy-traefik","Ports":"","Replicas":"1/1"} +{"ID":"rsxe3l71h9y4","Image":"esports-manager-api-eg8t7w:latest","Mode":"replicated","Name":"esports-manager-api-eg8t7w","Ports":"","Replicas":"1/1"} +{"ID":"fw52vzcw5dc0","Image":"team-synix-admin-dvgspy:latest","Mode":"replicated","Name":"team-synix-admin-dvgspy","Ports":"","Replicas":"1/1"} +{"ID":"551bwmtd6b4t","Image":"team-synix-leaderboard-9vx8ca:latest","Mode":"replicated","Name":"team-synix-leaderboard-9vx8ca","Ports":"","Replicas":"1/1"} +{"ID":"h1eyg3g1tyn3","Image":"postgres:15","Mode":"replicated","Name":"team-synix-webpage-db-fkivnf","Ports":"","Replicas":"1/1"}`; + + // if (stderr) { + // console.error(`Error: ${stderr}`); + // return; + // } + + const appArray = stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + console.log(appArray); + return appArray; + } catch (error) {} +}; + +export const getApplicationInfo = async (appName: string) => { + try { + // TODO: Implement this + // const { stdout, stderr } = await execAsync( + // `docker service ps ${appName} --format '{{json .}}'` + // ); + + const stdout = `{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"nx8jxlmb8niw","Image":"postgres:16","Name":"dokploy-postgres.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"s288g9lwtvi4","Image":"redis:7","Name":"dokploy-redis.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"2vcmejz51b23","Image":"traefik:v3.1.2","Name":"dokploy-traefik.1","Node":"v2202411192718297480","Ports":"*:80-\u003e80/tcp,*:80-\u003e80/tcp,*:443-\u003e443/tcp,*:443-\u003e443/tcp"} +{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"79iatnbsm2um","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":"*:3000-\u003e3000/tcp,*:3000-\u003e3000/tcp"} +{"CurrentState":"Shutdown 26 hours ago","DesiredState":"Shutdown","Error":"","ID":"zcwxs501zs7w","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"t59qhkoenno4","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"o5xtcuj6um7e","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"q1lr9rmf452g","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Shutdown 2 weeks ago","DesiredState":"Shutdown","Error":"","ID":"y9ixpg6b8qdo","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 24 hours ago","DesiredState":"Running","Error":"","ID":"xgcb919qjg1a","Image":"team-synix-admin-dvgspy:latest","Name":"team-synix-admin-dvgspy.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"7yi95wh8zhh6","Image":"team-synix-leaderboard-9vx8ca:latest","Name":"team-synix-leaderboard-9vx8ca.1","Node":"v2202411192718297480","Ports":""} +{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"89yzsnghpbq6","Image":"postgres:15","Name":"team-synix-webpage-db-fkivnf.1","Node":"v2202411192718297480","Ports":""}`; + + // if (stderr) { + // console.error(`Error: ${stderr}`); + // return; + // } + + const appArray = stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + + return appArray; + } catch (error) {} +}; From 04d3eb9ec01f246bec10b705dd562d4f64ad1efb Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:05:02 +0100 Subject: [PATCH 08/85] feat: add swarm tab and dashboard page for managing Docker swarm --- .../components/layouts/navigation-tabs.tsx | 298 +++++++++--------- apps/dokploy/pages/dashboard/swarm.tsx | 81 +++++ 2 files changed, 235 insertions(+), 144 deletions(-) create mode 100644 apps/dokploy/pages/dashboard/swarm.tsx diff --git a/apps/dokploy/components/layouts/navigation-tabs.tsx b/apps/dokploy/components/layouts/navigation-tabs.tsx index ab3dafcaa..c9392b10e 100644 --- a/apps/dokploy/components/layouts/navigation-tabs.tsx +++ b/apps/dokploy/components/layouts/navigation-tabs.tsx @@ -7,166 +7,176 @@ import { useEffect, useMemo, useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; interface TabInfo { - label: string; - tabLabel?: string; - description: string; - index: string; - type: TabState; - isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; + label: string; + tabLabel?: string; + description: string; + index: string; + type: TabState; + isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; } export type TabState = - | "projects" - | "monitoring" - | "settings" - | "traefik" - | "requests" - | "docker"; + | "projects" + | "monitoring" + | "settings" + | "traefik" + | "requests" + | "docker" + | "swarm"; const getTabMaps = (isCloud: boolean) => { - const elements: TabInfo[] = [ - { - label: "Projects", - description: "Manage your projects", - index: "/dashboard/projects", - type: "projects", - }, - ]; + const elements: TabInfo[] = [ + { + label: "Projects", + description: "Manage your projects", + index: "/dashboard/projects", + type: "projects", + }, + ]; - if (!isCloud) { - elements.push( - { - label: "Monitoring", - description: "Monitor your projects", - index: "/dashboard/monitoring", - type: "monitoring", - }, - { - label: "Traefik", - tabLabel: "Traefik File System", - description: "Manage your traefik", - index: "/dashboard/traefik", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); - }, - type: "traefik", - }, - { - label: "Docker", - description: "Manage your docker", - index: "/dashboard/docker", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "docker", - }, - { - label: "Requests", - description: "Manage your requests", - index: "/dashboard/requests", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "requests", - }, - ); - } + if (!isCloud) { + elements.push( + { + label: "Monitoring", + description: "Monitor your projects", + index: "/dashboard/monitoring", + type: "monitoring", + }, + { + label: "Traefik", + tabLabel: "Traefik File System", + description: "Manage your traefik", + index: "/dashboard/traefik", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); + }, + type: "traefik", + }, + { + label: "Docker", + description: "Manage your docker", + index: "/dashboard/docker", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "docker", + }, + { + label: "Swarm", + description: "Manage your docker swarm", + index: "/dashboard/swarm", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "swarm", + }, + { + label: "Requests", + description: "Manage your requests", + index: "/dashboard/requests", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "requests", + } + ); + } - elements.push({ - label: "Settings", - description: "Manage your settings", - type: "settings", - index: isCloud - ? "/dashboard/settings/profile" - : "/dashboard/settings/server", - }); + elements.push({ + label: "Settings", + description: "Manage your settings", + type: "settings", + index: isCloud + ? "/dashboard/settings/profile" + : "/dashboard/settings/server", + }); - return elements; + return elements; }; interface Props { - tab: TabState; - children: React.ReactNode; + tab: TabState; + children: React.ReactNode; } export const NavigationTabs = ({ tab, children }: Props) => { - const router = useRouter(); - const { data } = api.auth.get.useQuery(); - const [activeTab, setActiveTab] = useState(tab); - const { data: isCloud } = api.settings.isCloud.useQuery(); - const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: data?.id || "", - }, - { - enabled: !!data?.id && data?.rol === "user", - }, - ); + const router = useRouter(); + const { data } = api.auth.get.useQuery(); + const [activeTab, setActiveTab] = useState(tab); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); + const { data: user } = api.user.byAuthId.useQuery( + { + authId: data?.id || "", + }, + { + enabled: !!data?.id && data?.rol === "user", + } + ); - useEffect(() => { - setActiveTab(tab); - }, [tab]); + useEffect(() => { + setActiveTab(tab); + }, [tab]); - const activeTabInfo = useMemo(() => { - return tabMap.find((tab) => tab.type === activeTab); - }, [activeTab]); + const activeTabInfo = useMemo(() => { + return tabMap.find((tab) => tab.type === activeTab); + }, [activeTab]); - return ( -
-
-
-

- {activeTabInfo?.label} -

-

- {activeTabInfo?.description} -

-
- {tab === "projects" && - (data?.rol === "admin" || user?.canCreateProjects) && } -
-
- { - setActiveTab(e as TabState); - const tab = tabMap.find((tab) => tab.type === e); - router.push(tab?.index || ""); - }} - > -
- - {tabMap.map((tab, index) => { - if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { - return null; - } - return ( - - - {tab?.tabLabel || tab?.label} - - {tab.type === activeTab && ( -
-
-
- )} - - ); - })} - -
+ return ( +
+
+
+

+ {activeTabInfo?.label} +

+

+ {activeTabInfo?.description} +

+
+ {tab === "projects" && + (data?.rol === "admin" || user?.canCreateProjects) && } +
+
+ { + setActiveTab(e as TabState); + const tab = tabMap.find((tab) => tab.type === e); + router.push(tab?.index || ""); + }} + > +
+ + {tabMap.map((tab, index) => { + if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { + return null; + } + return ( + + + {tab?.tabLabel || tab?.label} + + {tab.type === activeTab && ( +
+
+
+ )} + + ); + })} + +
- - {children} - - -
-
- ); + + {children} + + +
+
+ ); }; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx new file mode 100644 index 000000000..d353ceedd --- /dev/null +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -0,0 +1,81 @@ +import { ShowSwarm } from "@/components/dashboard/swarm/show/node-list"; +import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; +import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { appRouter } from "@/server/api/root"; +import { IS_CLOUD, validateRequest } from "@dokploy/server"; +import { createServerSideHelpers } from "@trpc/react-query/server"; +import type { GetServerSidePropsContext } from "next"; +import React, { type ReactElement } from "react"; +import superjson from "superjson"; + +const Dashboard = () => { + return ; +}; + +export default Dashboard; + +Dashboard.getLayout = (page: ReactElement) => { + return {page}; +}; +export async function getServerSideProps( + ctx: GetServerSidePropsContext<{ serviceId: string }> +) { + if (IS_CLOUD) { + return { + redirect: { + permanent: true, + destination: "/dashboard/projects", + }, + }; + } + const { user, session } = await validateRequest(ctx.req, ctx.res); + if (!user) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + const { req, res } = ctx; + + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session, + user: user, + }, + transformer: superjson, + }); + try { + await helpers.project.all.prefetch(); + const auth = await helpers.auth.get.fetch(); + + if (auth.rol === "user") { + const user = await helpers.user.byAuthId.fetch({ + authId: auth.id, + }); + + if (!user.canAccessToDocker) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + } + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; + } catch (error) { + return { + props: {}, + }; + } +} From 5716954665cfe59b07ecd684bb50c3f2bb0bef04 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:05:39 +0100 Subject: [PATCH 09/85] feat: add components for displaying swarm node details and applications --- .../dashboard/swarm/applications/columns.tsx | 218 ++++++++++++++ .../swarm/applications/data-table.tsx | 264 +++++++++++++++++ .../swarm/applications/show-applications.tsx | 122 ++++++++ .../dashboard/swarm/details/show-node.tsx | 54 ++++ .../dashboard/swarm/show/columns.tsx | 201 +++++++++++++ .../dashboard/swarm/show/data-table.tsx | 269 ++++++++++++++++++ .../dashboard/swarm/show/show-nodes.tsx | 16 ++ 7 files changed, 1144 insertions(+) create mode 100644 apps/dokploy/components/dashboard/swarm/applications/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/applications/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/details/show-node.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx diff --git a/apps/dokploy/components/dashboard/swarm/applications/columns.tsx b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx new file mode 100644 index 000000000..ba2d9e13f --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx @@ -0,0 +1,218 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { Badge } from "@/components/ui/badge"; +import { ShowNodeConfig } from "../details/show-node"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface ApplicationList { + ID: string; + Image: string; + Mode: string; + Name: string; + Ports: string; + Replicas: string; + CurrentState: string; + DesiredState: string; + Error: string; + Node: string; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "ID", + accessorFn: (row) => row.ID, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ID")}
; + }, + }, + { + accessorKey: "Name", + accessorFn: (row) => row.Name, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Name")}
; + }, + }, + { + accessorKey: "Image", + accessorFn: (row) => row.Image, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Image")}
; + }, + }, + { + accessorKey: "Mode", + accessorFn: (row) => row.Mode, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Mode")}
; + }, + }, + { + accessorKey: "CurrentState", + accessorFn: (row) => row.CurrentState, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const value = row.getValue("CurrentState") as string; + const valueStart = value.startsWith("Running") + ? "Running" + : value.startsWith("Shutdown") + ? "Shutdown" + : value; + return ( +
+ + {value} + +
+ ); + }, + }, + { + accessorKey: "DesiredState", + accessorFn: (row) => row.DesiredState, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("DesiredState")}
; + }, + }, + + { + accessorKey: "Replicas", + accessorFn: (row) => row.Replicas, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Replicas")}
; + }, + }, + + { + accessorKey: "Ports", + accessorFn: (row) => row.Ports, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Ports")}
; + }, + }, + { + accessorKey: "Errors", + accessorFn: (row) => row.Error, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Errors")}
; + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx new file mode 100644 index 000000000..1b192f7d0 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx @@ -0,0 +1,264 @@ +"use client"; + +import { + type ColumnFiltersState, + type SortingState, + type VisibilityState, + type ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import React from "react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { ChevronDown } from "lucide-react"; +import { Input } from "@/components/ui/input"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + {/* {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} */} +
+
+ )} +
+
+ {/*
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
*/} + {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx new file mode 100644 index 000000000..2ef632b9b --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { api } from "@/utils/api"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { DataTable } from "./data-table"; +import { columns } from "./columns"; +import { LoaderIcon } from "lucide-react"; + +interface Props { + nodeName: string; +} + +interface ApplicationList { + ID: string; + Image: string; + Mode: string; + Name: string; + Ports: string; + Replicas: string; + CurrentState: string; + DesiredState: string; + Error: string; + Node: string; +} + +const ShowNodeApplications = ({ nodeName }: Props) => { + const [loading, setLoading] = React.useState(true); + const { data: NodeApps, isLoading: NodeAppsLoading } = + api.swarm.getNodeApps.useQuery(); + + let applicationList = ""; + + if (NodeApps && NodeApps.length > 0) { + applicationList = NodeApps.map((app) => app.Name).join(" "); + } + + const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = + api.swarm.getAppInfos.useQuery({ appName: applicationList }); + + if (NodeAppsLoading || NodeAppDetailsLoading) { + return ( + + + e.preventDefault()} + > + + + + + ); + } + + if (!NodeApps || !NodeAppDetails) { + return
No data found
; + } + + const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { + const appDetails = + NodeAppDetails?.filter((detail) => + detail.Name.startsWith(`${app.Name}.`) + ) || []; + + if (appDetails.length === 0) { + return [ + { + ...app, + CurrentState: "N/A", + DesiredState: "N/A", + Error: "", + Node: "N/A", + Ports: app.Ports, + }, + ]; + } + + return appDetails.map((detail) => ({ + ...app, + CurrentState: detail.CurrentState, + DesiredState: detail.DesiredState, + Error: detail.Error, + Node: detail.Node, + Ports: detail.Ports || app.Ports, + })); + }); + + return ( + + + e.preventDefault()} + > + Show Applications + + + + + Node Applications + + See in detail the applications running on this node + + +
+ +
+ {/*
*/} +
+
+ ); +}; + +export default ShowNodeApplications; diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx new file mode 100644 index 000000000..9a0921526 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { api } from "@/utils/api"; + +interface Props { + nodeId: string; +} + +export const ShowNodeConfig = ({ nodeId }: Props) => { + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); + return ( + + + e.preventDefault()} + > + View Config + + + + + Node Config + + See in detail the metadata of this node + + +
+ +
+              {/* {JSON.stringify(data, null, 2)} */}
+              
+            
+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/swarm/show/columns.tsx b/apps/dokploy/components/dashboard/swarm/show/columns.tsx new file mode 100644 index 000000000..ba11d749a --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/columns.tsx @@ -0,0 +1,201 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import { Badge } from "@/components/ui/badge"; +import { ShowNodeConfig } from "../details/show-node"; +import ShowNodeApplications from "../applications/show-applications"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface SwarmList { + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "ID", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ID")}
; + }, + }, + { + accessorKey: "EngineVersion", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("EngineVersion")}
; + }, + }, + { + accessorKey: "Hostname", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Hostname")}
; + }, + }, + // { + // accessorKey: "Status", + // header: ({ column }) => { + // return ( + // + // ); + // }, + // cell: ({ row }) => { + // const value = row.getValue("status") as string; + // return ( + //
+ // + // {value} + // + //
+ // ); + // }, + // }, + { + accessorKey: "Availability", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const value = row.getValue("Availability") as string; + return ( +
+ + {value} + +
+ ); + }, + }, + { + accessorKey: "ManagerStatus", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{row.getValue("ManagerStatus")}
+ ), + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ( + + + + + + Actions + + + {/* + View Logs + + + + Terminal + */} + + + ); + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/show/data-table.tsx b/apps/dokploy/components/dashboard/swarm/show/data-table.tsx new file mode 100644 index 000000000..d3e993529 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/data-table.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { + type ColumnFiltersState, + type SortingState, + type VisibilityState, + type ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import React from "react"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { ChevronDown } from "lucide-react"; +import { Input } from "@/components/ui/input"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + isLoading: boolean; +} + +export function DataTable({ + columns, + data, + isLoading, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + console.log("Data in DataTable", data); + + return ( +
+
+
+ + table.getColumn("Hostname")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ {/* + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
*/} +
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
+ {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx new file mode 100644 index 000000000..6c5bd99d6 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { SwarmList, columns } from "./columns"; +import { DataTable } from "./data-table"; +import { api } from "@/utils/api"; + +function ShowSwarmNodes() { + const { data, isLoading } = api.swarm.getNodes.useQuery(); + + console.log(data); + + return ( + + ); +} + +export default ShowSwarmNodes; From 813da8f8110ecd3040bb5938c8dbdf0a22344a66 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:06:08 +0100 Subject: [PATCH 10/85] refactor: clean up code formatting and improve readability in swarm dashboard --- apps/dokploy/pages/dashboard/swarm.tsx | 117 ++++++++++++------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index d353ceedd..b608efc01 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,4 +1,3 @@ -import { ShowSwarm } from "@/components/dashboard/swarm/show/node-list"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { appRouter } from "@/server/api/root"; @@ -9,73 +8,73 @@ import React, { type ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { - return ; + return ; }; export default Dashboard; Dashboard.getLayout = (page: ReactElement) => { - return {page}; + return {page}; }; export async function getServerSideProps( - ctx: GetServerSidePropsContext<{ serviceId: string }> + ctx: GetServerSidePropsContext<{ serviceId: string }>, ) { - if (IS_CLOUD) { - return { - redirect: { - permanent: true, - destination: "/dashboard/projects", - }, - }; - } - const { user, session } = await validateRequest(ctx.req, ctx.res); - if (!user) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - const { req, res } = ctx; + if (IS_CLOUD) { + return { + redirect: { + permanent: true, + destination: "/dashboard/projects", + }, + }; + } + const { user, session } = await validateRequest(ctx.req, ctx.res); + if (!user) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + const { req, res } = ctx; - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - req: req as any, - res: res as any, - db: null as any, - session: session, - user: user, - }, - transformer: superjson, - }); - try { - await helpers.project.all.prefetch(); - const auth = await helpers.auth.get.fetch(); + const helpers = createServerSideHelpers({ + router: appRouter, + ctx: { + req: req as any, + res: res as any, + db: null as any, + session: session, + user: user, + }, + transformer: superjson, + }); + try { + await helpers.project.all.prefetch(); + const auth = await helpers.auth.get.fetch(); - if (auth.rol === "user") { - const user = await helpers.user.byAuthId.fetch({ - authId: auth.id, - }); + if (auth.rol === "user") { + const user = await helpers.user.byAuthId.fetch({ + authId: auth.id, + }); - if (!user.canAccessToDocker) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - } - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; - } catch (error) { - return { - props: {}, - }; - } + if (!user.canAccessToDocker) { + return { + redirect: { + permanent: true, + destination: "/", + }, + }; + } + } + return { + props: { + trpcState: helpers.dehydrate(), + }, + }; + } catch (error) { + return { + props: {}, + }; + } } From 3fc5bfc5c51a2d6e295d0ee7ccd029e4a4c94d0e Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:11:43 +0100 Subject: [PATCH 11/85] feat: implement fetching of Docker service applications and their details --- packages/server/src/services/docker.ts | 57 ++++++++------------------ 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 8681cb228..c13c71e5f 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -265,59 +265,34 @@ export const getNodeInfo = async (nodeId: string) => { export const getNodeApplications = async () => { try { - // TODO: Implement this - // const { stdout, stderr } = await execAsync( - // `docker service ls --format '{{json .}}'` - // ); - - const stdout = `{"ID":"pxvnj68dxom9","Image":"dokploy/dokploy:latest","Mode":"replicated","Name":"dokploy","Ports":"","Replicas":"1/1"} -{"ID":"1sweo6dr2vrn","Image":"postgres:16","Mode":"replicated","Name":"dokploy-postgres","Ports":"","Replicas":"1/1"} -{"ID":"tnl2fck3rbop","Image":"redis:7","Mode":"replicated","Name":"dokploy-redis","Ports":"","Replicas":"1/1"} -{"ID":"o9ady4y1p96x","Image":"traefik:v3.1.2","Mode":"replicated","Name":"dokploy-traefik","Ports":"","Replicas":"1/1"} -{"ID":"rsxe3l71h9y4","Image":"esports-manager-api-eg8t7w:latest","Mode":"replicated","Name":"esports-manager-api-eg8t7w","Ports":"","Replicas":"1/1"} -{"ID":"fw52vzcw5dc0","Image":"team-synix-admin-dvgspy:latest","Mode":"replicated","Name":"team-synix-admin-dvgspy","Ports":"","Replicas":"1/1"} -{"ID":"551bwmtd6b4t","Image":"team-synix-leaderboard-9vx8ca:latest","Mode":"replicated","Name":"team-synix-leaderboard-9vx8ca","Ports":"","Replicas":"1/1"} -{"ID":"h1eyg3g1tyn3","Image":"postgres:15","Mode":"replicated","Name":"team-synix-webpage-db-fkivnf","Ports":"","Replicas":"1/1"}`; - - // if (stderr) { - // console.error(`Error: ${stderr}`); - // return; - // } + const { stdout, stderr } = await execAsync( + `docker service ls --format '{{json .}}'`, + ); + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } const appArray = stdout .trim() .split("\n") .map((line) => JSON.parse(line)); - console.log(appArray); return appArray; } catch (error) {} }; export const getApplicationInfo = async (appName: string) => { try { - // TODO: Implement this - // const { stdout, stderr } = await execAsync( - // `docker service ps ${appName} --format '{{json .}}'` - // ); - - const stdout = `{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"nx8jxlmb8niw","Image":"postgres:16","Name":"dokploy-postgres.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"s288g9lwtvi4","Image":"redis:7","Name":"dokploy-redis.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"2vcmejz51b23","Image":"traefik:v3.1.2","Name":"dokploy-traefik.1","Node":"v2202411192718297480","Ports":"*:80-\u003e80/tcp,*:80-\u003e80/tcp,*:443-\u003e443/tcp,*:443-\u003e443/tcp"} -{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"79iatnbsm2um","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":"*:3000-\u003e3000/tcp,*:3000-\u003e3000/tcp"} -{"CurrentState":"Shutdown 26 hours ago","DesiredState":"Shutdown","Error":"","ID":"zcwxs501zs7w","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"t59qhkoenno4","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Shutdown 7 days ago","DesiredState":"Shutdown","Error":"","ID":"o5xtcuj6um7e","Image":"dokploy/dokploy:latest","Name":"dokploy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"q1lr9rmf452g","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Shutdown 2 weeks ago","DesiredState":"Shutdown","Error":"","ID":"y9ixpg6b8qdo","Image":"esports-manager-api-eg8t7w:latest","Name":"esports-manager-api-eg8t7w.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 24 hours ago","DesiredState":"Running","Error":"","ID":"xgcb919qjg1a","Image":"team-synix-admin-dvgspy:latest","Name":"team-synix-admin-dvgspy.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 26 hours ago","DesiredState":"Running","Error":"","ID":"7yi95wh8zhh6","Image":"team-synix-leaderboard-9vx8ca:latest","Name":"team-synix-leaderboard-9vx8ca.1","Node":"v2202411192718297480","Ports":""} -{"CurrentState":"Running 2 weeks ago","DesiredState":"Running","Error":"","ID":"89yzsnghpbq6","Image":"postgres:15","Name":"team-synix-webpage-db-fkivnf.1","Node":"v2202411192718297480","Ports":""}`; - - // if (stderr) { - // console.error(`Error: ${stderr}`); - // return; - // } + const { stdout, stderr } = await execAsync( + `docker service ps ${appName} --format '{{json .}}'`, + ); + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } const appArray = stdout .trim() From 763219e859dc2112343aedb778e535aacb5960c6 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 12:12:48 +0100 Subject: [PATCH 12/85] refactor: streamline imports and improve code formatting in ShowSwarmNodes component --- .../components/dashboard/swarm/show/show-nodes.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx index 6c5bd99d6..e629654f7 100644 --- a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx @@ -1,16 +1,14 @@ +import { api } from "@/utils/api"; import React from "react"; -import { SwarmList, columns } from "./columns"; +import { columns } from "./columns"; import { DataTable } from "./data-table"; -import { api } from "@/utils/api"; function ShowSwarmNodes() { - const { data, isLoading } = api.swarm.getNodes.useQuery(); - - console.log(data); + const { data, isLoading } = api.swarm.getNodes.useQuery(); - return ( - - ); + return ( + + ); } export default ShowSwarmNodes; From 850523626339a3de37847f0e3278a4c966c2e8b4 Mon Sep 17 00:00:00 2001 From: Shadow Date: Tue, 17 Dec 2024 12:59:01 -0600 Subject: [PATCH 13/85] feat: mongo replica sets --- packages/server/src/utils/databases/mongo.ts | 54 ++++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/packages/server/src/utils/databases/mongo.ts b/packages/server/src/utils/databases/mongo.ts index c1b0542df..3c9556f9c 100644 --- a/packages/server/src/utils/databases/mongo.ts +++ b/packages/server/src/utils/databases/mongo.ts @@ -30,15 +30,54 @@ export const buildMongo = async (mongo: MongoNested) => { mounts, } = mongo; - const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}${ - env ? `\n${env}` : "" - }`; + const initReplicaSet = ` +#!/bin/bash +mongod --port 27017 --replSet rs0 --bind_ip_all & +MONGOD_PID=$! + +# Wait for MongoDB to be ready +while ! mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; do + sleep 2 +done + +# Check if replica set is already initialized +REPLICA_STATUS=$(mongosh --quiet --eval "rs.status().ok || 0") + +if [ "$REPLICA_STATUS" != "1" ]; then + echo "Initializing replica set..." + mongosh --eval ' + rs.initiate({ + _id: "rs0", + members: [{ _id: 0, host: "localhost:27017", priority: 1 }] + }); + + // Wait for the replica set to initialize + while (!rs.isMaster().ismaster) { + sleep(1000); + } + + // Create root user after replica set is initialized and we are primary + db.getSiblingDB("admin").createUser({ + user: "${databaseUser}", + pwd: "${databasePassword}", + roles: ["root"] + }); + ' +else + echo "Replica set already initialized." +fi + +wait $MONGOD_PID`; + + const defaultMongoEnv = `MONGO_INITDB_DATABASE=admin\n${env ? `${env}` : ""}`; + const resources = calculateResources({ memoryLimit, memoryReservation, cpuLimit, cpuReservation, }); + const envVariables = prepareEnvironmentVariables( defaultMongoEnv, mongo.project.env, @@ -56,12 +95,8 @@ export const buildMongo = async (mongo: MongoNested) => { Image: dockerImage, Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(command - ? { - Command: ["/bin/sh"], - Args: ["-c", command], - } - : {}), + Command: ["/bin/bash"], + Args: ["-c", command ?? initReplicaSet], }, Networks: [{ Target: "dokploy-network" }], Resources: { @@ -90,6 +125,7 @@ export const buildMongo = async (mongo: MongoNested) => { : [], }, }; + try { const service = docker.getService(appName); const inspect = await service.inspect(); From f98f18b331cce7409ea0422b45f37ab28aa48c35 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 20:48:35 +0100 Subject: [PATCH 14/85] feat: add monitoring card --- .../dashboard/swarm/monitoring-card.tsx | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 apps/dokploy/components/dashboard/swarm/monitoring-card.tsx diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx new file mode 100644 index 000000000..88c96c24d --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -0,0 +1,198 @@ +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; +import { + Activity, + AlertCircle, + CheckCircle, + HelpCircle, + Loader2, + Server, +} from "lucide-react"; +import { NodeCard } from "./show/deatils-card"; + +export interface SwarmList { + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; +} + +interface SwarmMonitorCardProps { + nodes: SwarmList[]; +} + +export default function SwarmMonitorCard() { + const { data: nodes, isLoading } = api.swarm.getNodes.useQuery(); + + if (isLoading) { + return ( +
+ + + + + Docker Swarm Monitor + + + +
+ +
+
+
+
+ ); + } + + if (!nodes) { + return ( + + + + + Docker Swarm Monitor + + + +
+ Failed to load data +
+
+
+ ); + } + + console.log(nodes); + const totalNodes = nodes.length; + const activeNodesCount = nodes.filter( + (node) => node.Status === "Ready", + ).length; + const managerNodesCount = nodes.filter( + (node) => + node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable", + ).length; + + const activeNodes = nodes.filter((node) => node.Status === "Ready"); + const managerNodes = nodes.filter( + (node) => + node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable", + ); + + const getStatusIcon = (status: string) => { + switch (status) { + case "Ready": + return ; + case "Down": + return ; + default: + return ; + } + }; + + return ( +
+ + + + + Docker Swarm Monitor + + + +
+
+ Total Nodes: + {totalNodes} +
+
+ Active Nodes: + + + + + {activeNodesCount} / {totalNodes} + + + + {activeNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
+
+
+ {/* + {activeNodesCount} / {totalNodes} + */} +
+
+ Manager Nodes: + + + + + {managerNodesCount} / {totalNodes} + + + + {managerNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
+
+
+ {/* + {managerNodes} / {totalNodes} + */} +
+
+

Node Status:

+
    + {nodes.map((node) => ( +
  • + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + +
  • + ))} +
+
+
+
+
+
+ {nodes.map((node) => ( + + ))} +
+
+ ); +} From e3ee89104bdd3b642a6d831316d20efcb77529e1 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 20:48:56 +0100 Subject: [PATCH 15/85] chore: remove tables and add new cards --- .../swarm/applications/data-table.tsx | 409 ++++++++---------- .../swarm/applications/show-applications.tsx | 191 ++++---- .../dashboard/swarm/containers/columns.tsx | 139 ++++++ .../dashboard/swarm/containers/data-table.tsx | 210 +++++++++ .../swarm/containers/show-container.tsx | 48 ++ .../dashboard/swarm/details/show-node.tsx | 94 ++-- .../dashboard/swarm/servers/columns.tsx | 168 +++++++ .../dashboard/swarm/servers/data-table.tsx | 210 +++++++++ .../dashboard/swarm/servers/show-server.tsx | 16 + .../dashboard/swarm/show/columns.tsx | 333 +++++++------- .../dashboard/swarm/show/deatils-card.tsx | 124 ++++++ .../components/layouts/navigation-tabs.tsx | 308 ++++++------- apps/dokploy/pages/dashboard/swarm.tsx | 19 +- 13 files changed, 1568 insertions(+), 701 deletions(-) create mode 100644 apps/dokploy/components/dashboard/swarm/containers/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/containers/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/containers/show-container.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/columns.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/data-table.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/show-server.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx diff --git a/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx index 1b192f7d0..03915c19b 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/data-table.tsx @@ -1,156 +1,160 @@ "use client"; import { - type ColumnFiltersState, - type SortingState, - type VisibilityState, - type ColumnDef, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, } from "@tanstack/react-table"; +import { Button } from "@/components/ui/button"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import React from "react"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, } from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { Button } from "@/components/ui/button"; import { ChevronDown } from "lucide-react"; -import { Input } from "@/components/ui/input"; +import React from "react"; interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; + columns: ColumnDef[]; + data: TData[]; } export function DataTable({ - columns, - data, + columns, + data, }: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [pagination, setPagination] = React.useState({ + pageIndex: 0, //initial page index + pageSize: 8, //default page size + }); - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); - return ( -
-
-
- - table.getColumn("Name")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - No results. - {/* {isLoading ? ( + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + {/* {isLoading ? (
Loading... @@ -159,106 +163,35 @@ export function DataTable({ ) : ( <>No results. )} */} - - - )} - -
- {/*
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
*/} - {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); + + + )} + + + + {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); } diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx index 2ef632b9b..4363adc1f 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -1,122 +1,117 @@ -import React from "react"; +import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; -import { api } from "@/utils/api"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { DataTable } from "./data-table"; +import { api } from "@/utils/api"; +import { Layers, LoaderIcon } from "lucide-react"; +import React from "react"; import { columns } from "./columns"; -import { LoaderIcon } from "lucide-react"; +import { DataTable } from "./data-table"; interface Props { - nodeName: string; + nodeName: string; } interface ApplicationList { - ID: string; - Image: string; - Mode: string; - Name: string; - Ports: string; - Replicas: string; - CurrentState: string; - DesiredState: string; - Error: string; - Node: string; + ID: string; + Image: string; + Mode: string; + Name: string; + Ports: string; + Replicas: string; + CurrentState: string; + DesiredState: string; + Error: string; + Node: string; } const ShowNodeApplications = ({ nodeName }: Props) => { - const [loading, setLoading] = React.useState(true); - const { data: NodeApps, isLoading: NodeAppsLoading } = - api.swarm.getNodeApps.useQuery(); + const [loading, setLoading] = React.useState(true); + const { data: NodeApps, isLoading: NodeAppsLoading } = + api.swarm.getNodeApps.useQuery(); - let applicationList = ""; + let applicationList = ""; - if (NodeApps && NodeApps.length > 0) { - applicationList = NodeApps.map((app) => app.Name).join(" "); - } + if (NodeApps && NodeApps.length > 0) { + applicationList = NodeApps.map((app) => app.Name).join(" "); + } - const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = - api.swarm.getAppInfos.useQuery({ appName: applicationList }); + const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = + api.swarm.getAppInfos.useQuery({ appName: applicationList }); - if (NodeAppsLoading || NodeAppDetailsLoading) { - return ( - - - e.preventDefault()} - > - - - - - ); - } + if (NodeAppsLoading || NodeAppDetailsLoading) { + return ( + + + + + + ); + } - if (!NodeApps || !NodeAppDetails) { - return
No data found
; - } + if (!NodeApps || !NodeAppDetails) { + return
No data found
; + } - const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { - const appDetails = - NodeAppDetails?.filter((detail) => - detail.Name.startsWith(`${app.Name}.`) - ) || []; + const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { + const appDetails = + NodeAppDetails?.filter((detail) => + detail.Name.startsWith(`${app.Name}.`), + ) || []; - if (appDetails.length === 0) { - return [ - { - ...app, - CurrentState: "N/A", - DesiredState: "N/A", - Error: "", - Node: "N/A", - Ports: app.Ports, - }, - ]; - } + if (appDetails.length === 0) { + return [ + { + ...app, + CurrentState: "N/A", + DesiredState: "N/A", + Error: "", + Node: "N/A", + Ports: app.Ports, + }, + ]; + } - return appDetails.map((detail) => ({ - ...app, - CurrentState: detail.CurrentState, - DesiredState: detail.DesiredState, - Error: detail.Error, - Node: detail.Node, - Ports: detail.Ports || app.Ports, - })); - }); + return appDetails.map((detail) => ({ + ...app, + CurrentState: detail.CurrentState, + DesiredState: detail.DesiredState, + Error: detail.Error, + Node: detail.Node, + Ports: detail.Ports || app.Ports, + })); + }); - return ( - - - e.preventDefault()} - > - Show Applications - - - - - Node Applications - - See in detail the applications running on this node - - -
- -
- {/*
*/} -
-
- ); + return ( + + + + + + + Node Applications + + See in detail the applications running on this node + + +
+ +
+ {/*
*/} +
+
+ ); }; export default ShowNodeApplications; diff --git a/apps/dokploy/components/dashboard/swarm/containers/columns.tsx b/apps/dokploy/components/dashboard/swarm/containers/columns.tsx new file mode 100644 index 000000000..0ccf8e373 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/columns.tsx @@ -0,0 +1,139 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import type { Badge } from "@/components/ui/badge"; +import { ShowNodeConfig } from "../details/show-node"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface ContainerList { + containerId: string; + name: string; + image: string; + ports: string; + state: string; + status: string; + serverId: string | null | undefined; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "ID", + accessorFn: (row) => row.containerId, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("containerId")}
; + }, + }, + { + accessorKey: "Name", + accessorFn: (row) => row.name, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("name")}
; + }, + }, + { + accessorKey: "Image", + accessorFn: (row) => row.image, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("image")}
; + }, + }, + { + accessorKey: "Ports", + accessorFn: (row) => row.ports, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ports")}
; + }, + }, + { + accessorKey: "State", + accessorFn: (row) => row.state, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("state")}
; + }, + }, + { + accessorKey: "Status", + accessorFn: (row) => row.status, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("status")}
; + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx new file mode 100644 index 000000000..95b4498ea --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import React from "react"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + isLoading: boolean; +} + +export function DataTable({ + columns, + data, + isLoading, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ +
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
+ {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx new file mode 100644 index 000000000..dec6c6e74 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx @@ -0,0 +1,48 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { api } from "@/utils/api"; +import React from "react"; +import { ShowContainers } from "../../docker/show/show-containers"; +import { columns } from "./columns"; +import { DataTable } from "./data-table"; +// import { columns } from "./columns"; +// import { DataTable } from "./data-table"; + +interface Props { + serverId: string; +} + +const ShowNodeContainers = ({ serverId }: Props) => { + return ( + + + e.preventDefault()} + > + Show Container + + + + + Node Container + + See all containers running on this node + + +
+ +
+
+
+ ); +}; + +export default ShowNodeContainers; diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx index 9a0921526..4f7518056 100644 --- a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/show-node.tsx @@ -1,54 +1,60 @@ -import React from "react"; +import { CodeEditor } from "@/components/shared/code-editor"; +import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { CodeEditor } from "@/components/shared/code-editor"; import { api } from "@/utils/api"; +import { Settings } from "lucide-react"; +import React from "react"; interface Props { - nodeId: string; + nodeId: string; } export const ShowNodeConfig = ({ nodeId }: Props) => { - const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); - return ( - - - e.preventDefault()} - > - View Config - - - - - Node Config - - See in detail the metadata of this node - - -
- -
-              {/* {JSON.stringify(data, null, 2)} */}
-              
-            
-
-
-
-
- ); + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); + return ( + + + {/* e.preventDefault()} + > + Show Config + */} + + + + + Node Config + + See in detail the metadata of this node + + +
+ +
+							{/* {JSON.stringify(data, null, 2)} */}
+							
+						
+
+
+
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/swarm/servers/columns.tsx b/apps/dokploy/components/dashboard/swarm/servers/columns.tsx new file mode 100644 index 000000000..02b013db0 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/columns.tsx @@ -0,0 +1,168 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +import type { Badge } from "@/components/ui/badge"; +import { ShowContainers } from "../../docker/show/show-containers"; +import ShowNodeContainers from "../containers/show-container"; +import { ShowNodeConfig } from "../details/show-node"; +// import { ShowContainerConfig } from "../config/show-container-config"; +// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; +// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; +// import type { Container } from "./show-containers"; + +export interface ServerList { + totalSum: number; + serverId: string; + name: string; + description: string | null; + ipAddress: string; + port: number; + username: string; + appName: string; + enableDockerCleanup: boolean; + createdAt: string; + adminId: string; + serverStatus: "active" | "inactive"; + command: string; + sshKeyId: string | null; +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "serverId", + accessorFn: (row) => row.serverId, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("serverId")}
; + }, + }, + { + accessorKey: "name", + accessorFn: (row) => row.name, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("name")}
; + }, + }, + { + accessorKey: "ipAddress", + accessorFn: (row) => row.ipAddress, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ipAddress")}
; + }, + }, + { + accessorKey: "port", + accessorFn: (row) => row.port, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("port")}
; + }, + }, + { + accessorKey: "username", + accessorFn: (row) => row.username, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("username")}
; + }, + }, + { + accessorKey: "createdAt", + accessorFn: (row) => row.createdAt, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("createdAt")}
; + }, + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ( + + + + + + Actions + + + + ); + }, + }, +]; diff --git a/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx new file mode 100644 index 000000000..95b4498ea --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { + type ColumnDef, + type ColumnFiltersState, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import React from "react"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + isLoading: boolean; +} + +export function DataTable({ + columns, + data, + isLoading, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+
+ + table.getColumn("Name")?.setFilterValue(event.target.value) + } + className="md:max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+ +
+ {isLoading ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + {isLoading ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} +
+
+ )} +
+ {data && data?.length > 0 && ( +
+
+ + +
+
+ )} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx b/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx new file mode 100644 index 000000000..0486b1648 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx @@ -0,0 +1,16 @@ +import { api } from "@/utils/api"; +import React from "react"; +import { columns } from "./columns"; +import { DataTable } from "./data-table"; + +function ShowApplicationServers() { + const { data, isLoading } = api.server.all.useQuery(); + + console.log(data); + + return ( + + ); +} + +export default ShowApplicationServers; diff --git a/apps/dokploy/components/dashboard/swarm/show/columns.tsx b/apps/dokploy/components/dashboard/swarm/show/columns.tsx index ba11d749a..b07749363 100644 --- a/apps/dokploy/components/dashboard/swarm/show/columns.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/columns.tsx @@ -4,180 +4,181 @@ import * as React from "react"; import { Button } from "@/components/ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; -import { ShowNodeConfig } from "../details/show-node"; import ShowNodeApplications from "../applications/show-applications"; +import ShowContainers from "../containers/show-container"; +import { ShowNodeConfig } from "../details/show-node"; // import { ShowContainerConfig } from "../config/show-container-config"; // import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; // import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; // import type { Container } from "./show-containers"; export interface SwarmList { - ID: string; - Hostname: string; - Availability: string; - EngineVersion: string; - Status: string; - ManagerStatus: string; - TLSStatus: string; + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; } export const columns: ColumnDef[] = [ - { - accessorKey: "ID", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ID")}
; - }, - }, - { - accessorKey: "EngineVersion", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("EngineVersion")}
; - }, - }, - { - accessorKey: "Hostname", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("Hostname")}
; - }, - }, - // { - // accessorKey: "Status", - // header: ({ column }) => { - // return ( - // - // ); - // }, - // cell: ({ row }) => { - // const value = row.getValue("status") as string; - // return ( - //
- // - // {value} - // - //
- // ); - // }, - // }, - { - accessorKey: "Availability", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const value = row.getValue("Availability") as string; - return ( -
- - {value} - -
- ); - }, - }, - { - accessorKey: "ManagerStatus", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => ( -
{row.getValue("ManagerStatus")}
- ), - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - return ( - - - - - - Actions - - - {/* { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("ID")}
; + }, + }, + { + accessorKey: "EngineVersion", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("EngineVersion")}
; + }, + }, + { + accessorKey: "Hostname", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return
{row.getValue("Hostname")}
; + }, + }, + // { + // accessorKey: "Status", + // header: ({ column }) => { + // return ( + // + // ); + // }, + // cell: ({ row }) => { + // const value = row.getValue("status") as string; + // return ( + //
+ // + // {value} + // + //
+ // ); + // }, + // }, + { + accessorKey: "Availability", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const value = row.getValue("Availability") as string; + return ( +
+ + {value} + +
+ ); + }, + }, + { + accessorKey: "ManagerStatus", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{row.getValue("ManagerStatus")}
+ ), + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ( + + + + + + Actions + + + {/* @@ -193,9 +194,9 @@ export const columns: ColumnDef[] = [ > Terminal */} - - - ); - }, - }, +
+
+ ); + }, + }, ]; diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx new file mode 100644 index 000000000..1d12ab52a --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx @@ -0,0 +1,124 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + AlertCircle, + CheckCircle, + HelpCircle, + Layers, + Settings, +} from "lucide-react"; +import { useState } from "react"; +import ShowNodeApplications from "../applications/show-applications"; +import { ShowNodeConfig } from "../details/show-node"; + +export interface SwarmList { + ID: string; + Hostname: string; + Availability: string; + EngineVersion: string; + Status: string; + ManagerStatus: string; + TLSStatus: string; +} + +interface NodeCardProps { + node: SwarmList; +} + +export function NodeCard({ node }: NodeCardProps) { + const [showConfig, setShowConfig] = useState(false); + const [showServices, setShowServices] = useState(false); + + const getStatusIcon = (status: string) => { + switch (status) { + case "Ready": + return ; + case "Down": + return ; + default: + return ; + } + }; + + return ( + + + + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + + + + +
+
+ Status: + {node.Status} +
+
+ Availability: + {node.Availability} +
+
+ Engine Version: + {node.EngineVersion} +
+
+ TLS Status: + {node.TLSStatus} +
+
+
+ + {/* + + + + + + Node Configuration + +
+
+									{JSON.stringify(node, null, 2)}
+								
+
+
+
*/} + + {/* + + + + + + Node Services + +
+

Service information would be displayed here.

+
+
+
*/} +
+
+
+ ); +} diff --git a/apps/dokploy/components/layouts/navigation-tabs.tsx b/apps/dokploy/components/layouts/navigation-tabs.tsx index c9392b10e..46e590a73 100644 --- a/apps/dokploy/components/layouts/navigation-tabs.tsx +++ b/apps/dokploy/components/layouts/navigation-tabs.tsx @@ -7,176 +7,176 @@ import { useEffect, useMemo, useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; interface TabInfo { - label: string; - tabLabel?: string; - description: string; - index: string; - type: TabState; - isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; + label: string; + tabLabel?: string; + description: string; + index: string; + type: TabState; + isShow?: ({ rol, user }: { rol?: Auth["rol"]; user?: User }) => boolean; } export type TabState = - | "projects" - | "monitoring" - | "settings" - | "traefik" - | "requests" - | "docker" - | "swarm"; + | "projects" + | "monitoring" + | "settings" + | "traefik" + | "requests" + | "docker" + | "swarm"; const getTabMaps = (isCloud: boolean) => { - const elements: TabInfo[] = [ - { - label: "Projects", - description: "Manage your projects", - index: "/dashboard/projects", - type: "projects", - }, - ]; + const elements: TabInfo[] = [ + { + label: "Projects", + description: "Manage your projects", + index: "/dashboard/projects", + type: "projects", + }, + ]; - if (!isCloud) { - elements.push( - { - label: "Monitoring", - description: "Monitor your projects", - index: "/dashboard/monitoring", - type: "monitoring", - }, - { - label: "Traefik", - tabLabel: "Traefik File System", - description: "Manage your traefik", - index: "/dashboard/traefik", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); - }, - type: "traefik", - }, - { - label: "Docker", - description: "Manage your docker", - index: "/dashboard/docker", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "docker", - }, - { - label: "Swarm", - description: "Manage your docker swarm", - index: "/dashboard/swarm", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "swarm", - }, - { - label: "Requests", - description: "Manage your requests", - index: "/dashboard/requests", - isShow: ({ rol, user }) => { - return Boolean(rol === "admin" || user?.canAccessToDocker); - }, - type: "requests", - } - ); - } + if (!isCloud) { + elements.push( + { + label: "Monitoring", + description: "Monitor your projects", + index: "/dashboard/monitoring", + type: "monitoring", + }, + { + label: "Traefik", + tabLabel: "Traefik File System", + description: "Manage your traefik", + index: "/dashboard/traefik", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToTraefikFiles); + }, + type: "traefik", + }, + { + label: "Docker", + description: "Manage your docker", + index: "/dashboard/docker", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "docker", + }, + { + label: "Swarm & Server", + description: "Manage your docker swarm and Servers", + index: "/dashboard/swarm", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "swarm", + }, + { + label: "Requests", + description: "Manage your requests", + index: "/dashboard/requests", + isShow: ({ rol, user }) => { + return Boolean(rol === "admin" || user?.canAccessToDocker); + }, + type: "requests", + }, + ); + } - elements.push({ - label: "Settings", - description: "Manage your settings", - type: "settings", - index: isCloud - ? "/dashboard/settings/profile" - : "/dashboard/settings/server", - }); + elements.push({ + label: "Settings", + description: "Manage your settings", + type: "settings", + index: isCloud + ? "/dashboard/settings/profile" + : "/dashboard/settings/server", + }); - return elements; + return elements; }; interface Props { - tab: TabState; - children: React.ReactNode; + tab: TabState; + children: React.ReactNode; } export const NavigationTabs = ({ tab, children }: Props) => { - const router = useRouter(); - const { data } = api.auth.get.useQuery(); - const [activeTab, setActiveTab] = useState(tab); - const { data: isCloud } = api.settings.isCloud.useQuery(); - const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: data?.id || "", - }, - { - enabled: !!data?.id && data?.rol === "user", - } - ); + const router = useRouter(); + const { data } = api.auth.get.useQuery(); + const [activeTab, setActiveTab] = useState(tab); + const { data: isCloud } = api.settings.isCloud.useQuery(); + const tabMap = useMemo(() => getTabMaps(isCloud ?? false), [isCloud]); + const { data: user } = api.user.byAuthId.useQuery( + { + authId: data?.id || "", + }, + { + enabled: !!data?.id && data?.rol === "user", + }, + ); - useEffect(() => { - setActiveTab(tab); - }, [tab]); + useEffect(() => { + setActiveTab(tab); + }, [tab]); - const activeTabInfo = useMemo(() => { - return tabMap.find((tab) => tab.type === activeTab); - }, [activeTab]); + const activeTabInfo = useMemo(() => { + return tabMap.find((tab) => tab.type === activeTab); + }, [activeTab]); - return ( -
-
-
-

- {activeTabInfo?.label} -

-

- {activeTabInfo?.description} -

-
- {tab === "projects" && - (data?.rol === "admin" || user?.canCreateProjects) && } -
-
- { - setActiveTab(e as TabState); - const tab = tabMap.find((tab) => tab.type === e); - router.push(tab?.index || ""); - }} - > -
- - {tabMap.map((tab, index) => { - if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { - return null; - } - return ( - - - {tab?.tabLabel || tab?.label} - - {tab.type === activeTab && ( -
-
-
- )} - - ); - })} - -
+ return ( +
+
+
+

+ {activeTabInfo?.label} +

+

+ {activeTabInfo?.description} +

+
+ {tab === "projects" && + (data?.rol === "admin" || user?.canCreateProjects) && } +
+
+ { + setActiveTab(e as TabState); + const tab = tabMap.find((tab) => tab.type === e); + router.push(tab?.index || ""); + }} + > +
+ + {tabMap.map((tab, index) => { + if (tab?.isShow && !tab?.isShow?.({ rol: data?.rol, user })) { + return null; + } + return ( + + + {tab?.tabLabel || tab?.label} + + {tab.type === activeTab && ( +
+
+
+ )} + + ); + })} + +
- - {children} - - -
-
- ); + + {children} + + +
+
+ ); }; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index b608efc01..11035da77 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,6 +1,11 @@ +import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; +import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; +import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { Separator } from "@/components/ui/separator"; import { appRouter } from "@/server/api/root"; +import { api } from "@/utils/api"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; @@ -8,7 +13,19 @@ import React, { type ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { - return ; + return ( + <> +
+ +
+ + {/*

Swarm Nodes

+ + +

Server Nodes

+ */} + + ); }; export default Dashboard; From 3080926a505e8fbf9d54e950b447de29ed850725 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:07:30 +0100 Subject: [PATCH 16/85] feat: add new items --- .../dashboard/swarm/show/deatils-card.tsx | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx index 1d12ab52a..b622ba820 100644 --- a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx @@ -8,11 +8,13 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { api } from "@/utils/api"; import { AlertCircle, CheckCircle, HelpCircle, Layers, + LoaderIcon, Settings, } from "lucide-react"; import { useState } from "react"; @@ -37,6 +39,10 @@ export function NodeCard({ node }: NodeCardProps) { const [showConfig, setShowConfig] = useState(false); const [showServices, setShowServices] = useState(false); + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ + nodeId: node.ID, + }); + const getStatusIcon = (status: string) => { switch (status) { case "Ready": @@ -48,6 +54,30 @@ export function NodeCard({ node }: NodeCardProps) { } }; + if (isLoading) { + return ( + + + + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + + + + +
+ +
+
+
+ ); + } + + console.log(data); return ( @@ -67,6 +97,14 @@ export function NodeCard({ node }: NodeCardProps) { Status: {node.Status}
+
+ IP Address: + {isLoading ? ( + + ) : ( + {data.Status.Addr} + )} +
Availability: {node.Availability} @@ -75,6 +113,29 @@ export function NodeCard({ node }: NodeCardProps) { Engine Version: {node.EngineVersion}
+
+ CPU: + {isLoading ? ( + + ) : ( + + {(data.Description.Resources.NanoCPUs / 1e9).toFixed(2)} GHz + + )} +
+
+ Memory: + {isLoading ? ( + + ) : ( + + {(data.Description.Resources.MemoryBytes / 1024 ** 3).toFixed( + 2, + )}{" "} + GB + + )} +
TLS Status: {node.TLSStatus} From be237ae4cf0ff10f4fb1eb53b62d180b8ea6db3a Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:07:47 +0100 Subject: [PATCH 17/85] chore: comment out refresh for now --- .../dashboard/swarm/monitoring-card.tsx | 89 ++++++++++--------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 88c96c24d..f55ef07b3 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -102,14 +102,25 @@ export default function SwarmMonitorCard() { return (
- + Docker Swarm Monitor + {/* */} -
+
Total Nodes: {totalNodes} @@ -121,24 +132,23 @@ export default function SwarmMonitorCard() { {activeNodesCount} / {totalNodes} - {activeNodes.map((node) => ( -
- {getStatusIcon(node.Status)} - {node.Hostname} -
- ))} +
+ {activeNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
- {/* - {activeNodesCount} / {totalNodes} - */}
Manager Nodes: @@ -153,39 +163,38 @@ export default function SwarmMonitorCard() { - {managerNodes.map((node) => ( -
- {getStatusIcon(node.Status)} - {node.Hostname} -
- ))} +
+ {managerNodes.map((node) => ( +
+ {getStatusIcon(node.Status)} + {node.Hostname} +
+ ))} +
- {/* - {managerNodes} / {totalNodes} - */} -
-
-

Node Status:

-
    - {nodes.map((node) => ( -
  • - - {getStatusIcon(node.Status)} - {node.Hostname} - - - {node.ManagerStatus || "Worker"} - -
  • - ))} -
+
+

Node Status:

+
    + {nodes.map((node) => ( +
  • + + {getStatusIcon(node.Status)} + {node.Hostname} + + + {node.ManagerStatus || "Worker"} + +
  • + ))} +
+
From 577b126e66046deaace6a8899fe9eff38c90c1cb Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:19:38 +0100 Subject: [PATCH 18/85] feat: make bg transparent --- apps/dokploy/components/dashboard/swarm/monitoring-card.tsx | 4 +++- apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index f55ef07b3..16b6102a9 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -94,6 +94,8 @@ export default function SwarmMonitorCard() { return ; case "Down": return ; + case "Disconnected": + return ; default: return ; } @@ -101,7 +103,7 @@ export default function SwarmMonitorCard() { return (
- + diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx index b622ba820..83d226a5c 100644 --- a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx @@ -56,7 +56,7 @@ export function NodeCard({ node }: NodeCardProps) { if (isLoading) { return ( - + @@ -79,7 +79,7 @@ export function NodeCard({ node }: NodeCardProps) { console.log(data); return ( - + From 752c9f28185e6c39bb9612184b26f8ad7e291aaf Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 21:35:32 +0100 Subject: [PATCH 19/85] style: remove bg and border --- .../components/dashboard/swarm/containers/show-container.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx index dec6c6e74..4d6582aaa 100644 --- a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx +++ b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx @@ -37,7 +37,7 @@ const ShowNodeContainers = ({ serverId }: Props) => { See all containers running on this node -
+
From 9d497142db89d2571ef81a778eb5cd866d2a005d Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 08:43:07 +0100 Subject: [PATCH 20/85] feat: add latest cards --- .../dashboard/swarm/server-card.tsx | 122 ++++++++++++++++++ .../swarm/servers/servers-overview.tsx | 24 ++++ apps/dokploy/pages/dashboard/swarm.tsx | 5 +- 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/components/dashboard/swarm/server-card.tsx create mode 100644 apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx diff --git a/apps/dokploy/components/dashboard/swarm/server-card.tsx b/apps/dokploy/components/dashboard/swarm/server-card.tsx new file mode 100644 index 000000000..100291144 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/server-card.tsx @@ -0,0 +1,122 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { AlertCircle, CheckCircle, HelpCircle, ServerIcon } from "lucide-react"; +import { useState } from "react"; +import { ShowContainers } from "../docker/show/show-containers"; +// import type { Server } from "../types/server"; +// import { ShowServerContainers } from "./ShowServerContainers"; + +export interface Server { + serverId: string; + name: string; + description: string | null; + ipAddress: string; + port: number; + username: string; + appName: string; + enableDockerCleanup: boolean; + createdAt: string; + adminId: string; + serverStatus: "active" | "inactive"; + command: string; + sshKeyId: string | null; +} + +interface ServerOverviewCardProps { + server: Server; +} + +export function ServerOverviewCard({ server }: ServerOverviewCardProps) { + const [showContainers, setShowContainers] = useState(false); + + const getStatusIcon = (status: string) => { + switch (status) { + case "active": + return ; + case "inactive": + return ; + default: + return ; + } + }; + + return ( + + + + + {getStatusIcon(server.serverStatus)} + {server.name} + + + {server.serverStatus} + + + + +
+
+ IP Address: + {server.ipAddress} +
+
+ Port: + {server.port} +
+
+ Username: + {server.username} +
+
+ App Name: + {server.appName} +
+
+ Docker Cleanup: + {server.enableDockerCleanup ? "Enabled" : "Disabled"} +
+
+ Created At: + {new Date(server.createdAt).toLocaleString()} +
+
+
+ + + + + + + + + {/* */} +
+ {/* {showContainers && ( +
+ +
+ )} */} +
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx new file mode 100644 index 000000000..8768a88c8 --- /dev/null +++ b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx @@ -0,0 +1,24 @@ +import { api } from "@/utils/api"; +import { ServerOverviewCard } from "../server-card"; + +export default function ServersOverview() { + const { data: servers, isLoading } = api.server.all.useQuery(); + + if (isLoading) { + return
Loading...
; + } + + if (!servers) { + return
No servers found
; + } + return ( +
+

Server Overview

+
+ {servers.map((server) => ( + + ))} +
+
+ ); +} diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 11035da77..24fa43268 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,5 +1,7 @@ import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; +import { ServerOverviewCard } from "@/components/dashboard/swarm/server-card"; +import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; @@ -18,7 +20,8 @@ const Dashboard = () => {
- + + {/* */} {/*

Swarm Nodes

From d4d74d38316defd53021ce60ecde5a60b34971bf Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 08:49:02 +0100 Subject: [PATCH 21/85] refactor: remove not needed import, move to better folder structure --- .../dashboard/swarm/applications/columns.tsx | 2 +- .../swarm/applications/show-applications.tsx | 2 - .../dashboard/swarm/containers/columns.tsx | 139 --------- .../dashboard/swarm/containers/data-table.tsx | 210 -------------- .../swarm/containers/show-container.tsx | 48 ---- .../swarm/{show => details}/deatils-card.tsx | 57 +--- .../{show-node.tsx => show-node-config.tsx} | 8 - .../dashboard/swarm/monitoring-card.tsx | 2 +- .../dashboard/swarm/servers/columns.tsx | 168 ----------- .../dashboard/swarm/servers/data-table.tsx | 210 -------------- .../swarm/{ => servers}/server-card.tsx | 21 +- .../swarm/servers/servers-overview.tsx | 2 +- .../dashboard/swarm/servers/show-server.tsx | 16 -- .../dashboard/swarm/show/columns.tsx | 202 ------------- .../dashboard/swarm/show/data-table.tsx | 269 ------------------ .../dashboard/swarm/show/show-nodes.tsx | 14 - apps/dokploy/pages/dashboard/swarm.tsx | 2 +- 17 files changed, 7 insertions(+), 1365 deletions(-) delete mode 100644 apps/dokploy/components/dashboard/swarm/containers/columns.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/containers/data-table.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/containers/show-container.tsx rename apps/dokploy/components/dashboard/swarm/{show => details}/deatils-card.tsx (69%) rename apps/dokploy/components/dashboard/swarm/details/{show-node.tsx => show-node-config.tsx} (85%) delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/columns.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/data-table.tsx rename apps/dokploy/components/dashboard/swarm/{ => servers}/server-card.tsx (81%) delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/show-server.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/show/columns.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/show/data-table.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx diff --git a/apps/dokploy/components/dashboard/swarm/applications/columns.tsx b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx index ba2d9e13f..1961cd997 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/columns.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/columns.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Badge } from "@/components/ui/badge"; -import { ShowNodeConfig } from "../details/show-node"; +import { ShowNodeConfig } from "../details/show-node-config"; // import { ShowContainerConfig } from "../config/show-container-config"; // import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; // import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx index 4363adc1f..e3b38a71d 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -7,7 +7,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { Layers, LoaderIcon } from "lucide-react"; import React from "react"; @@ -108,7 +107,6 @@ const ShowNodeApplications = ({ nodeName }: Props) => {
- {/*
*/} ); diff --git a/apps/dokploy/components/dashboard/swarm/containers/columns.tsx b/apps/dokploy/components/dashboard/swarm/containers/columns.tsx deleted file mode 100644 index 0ccf8e373..000000000 --- a/apps/dokploy/components/dashboard/swarm/containers/columns.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import type { ColumnDef } from "@tanstack/react-table"; -import { ArrowUpDown, MoreHorizontal } from "lucide-react"; -import * as React from "react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -import type { Badge } from "@/components/ui/badge"; -import { ShowNodeConfig } from "../details/show-node"; -// import { ShowContainerConfig } from "../config/show-container-config"; -// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; -// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; -// import type { Container } from "./show-containers"; - -export interface ContainerList { - containerId: string; - name: string; - image: string; - ports: string; - state: string; - status: string; - serverId: string | null | undefined; -} - -export const columns: ColumnDef[] = [ - { - accessorKey: "ID", - accessorFn: (row) => row.containerId, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("containerId")}
; - }, - }, - { - accessorKey: "Name", - accessorFn: (row) => row.name, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("name")}
; - }, - }, - { - accessorKey: "Image", - accessorFn: (row) => row.image, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("image")}
; - }, - }, - { - accessorKey: "Ports", - accessorFn: (row) => row.ports, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ports")}
; - }, - }, - { - accessorKey: "State", - accessorFn: (row) => row.state, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("state")}
; - }, - }, - { - accessorKey: "Status", - accessorFn: (row) => row.status, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("status")}
; - }, - }, -]; diff --git a/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx deleted file mode 100644 index 95b4498ea..000000000 --- a/apps/dokploy/components/dashboard/swarm/containers/data-table.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { ChevronDown } from "lucide-react"; -import React from "react"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - isLoading: boolean; -} - -export function DataTable({ - columns, - data, - isLoading, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - return ( -
-
-
- - table.getColumn("Name")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- -
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
- {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx b/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx deleted file mode 100644 index 4d6582aaa..000000000 --- a/apps/dokploy/components/dashboard/swarm/containers/show-container.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { api } from "@/utils/api"; -import React from "react"; -import { ShowContainers } from "../../docker/show/show-containers"; -import { columns } from "./columns"; -import { DataTable } from "./data-table"; -// import { columns } from "./columns"; -// import { DataTable } from "./data-table"; - -interface Props { - serverId: string; -} - -const ShowNodeContainers = ({ serverId }: Props) => { - return ( - - - e.preventDefault()} - > - Show Container - - - - - Node Container - - See all containers running on this node - - -
- -
-
-
- ); -}; - -export default ShowNodeContainers; diff --git a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx similarity index 69% rename from apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx rename to apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx index 83d226a5c..b8eb9f81f 100644 --- a/apps/dokploy/components/dashboard/swarm/show/deatils-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx @@ -1,25 +1,10 @@ import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { - AlertCircle, - CheckCircle, - HelpCircle, - Layers, - LoaderIcon, - Settings, -} from "lucide-react"; +import { AlertCircle, CheckCircle, HelpCircle, LoaderIcon } from "lucide-react"; import { useState } from "react"; import ShowNodeApplications from "../applications/show-applications"; -import { ShowNodeConfig } from "../details/show-node"; +import { ShowNodeConfig } from "./show-node-config"; export interface SwarmList { ID: string; @@ -36,9 +21,6 @@ interface NodeCardProps { } export function NodeCard({ node }: NodeCardProps) { - const [showConfig, setShowConfig] = useState(false); - const [showServices, setShowServices] = useState(false); - const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId: node.ID, }); @@ -77,7 +59,6 @@ export function NodeCard({ node }: NodeCardProps) { ); } - console.log(data); return ( @@ -143,41 +124,7 @@ export function NodeCard({ node }: NodeCardProps) {
- {/* - - - - - - Node Configuration - -
-
-									{JSON.stringify(node, null, 2)}
-								
-
-
-
*/} - {/* - - - - - - Node Services - -
-

Service information would be displayed here.

-
-
-
*/}
diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx similarity index 85% rename from apps/dokploy/components/dashboard/swarm/details/show-node.tsx rename to apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx index 4f7518056..2d8a3e3ee 100644 --- a/apps/dokploy/components/dashboard/swarm/details/show-node.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx @@ -8,10 +8,8 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { Settings } from "lucide-react"; -import React from "react"; interface Props { nodeId: string; @@ -22,12 +20,6 @@ export const ShowNodeConfig = ({ nodeId }: Props) => { return ( - {/* e.preventDefault()} - > - Show Config - */} - ); - }, - cell: ({ row }) => { - return
{row.getValue("serverId")}
; - }, - }, - { - accessorKey: "name", - accessorFn: (row) => row.name, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("name")}
; - }, - }, - { - accessorKey: "ipAddress", - accessorFn: (row) => row.ipAddress, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ipAddress")}
; - }, - }, - { - accessorKey: "port", - accessorFn: (row) => row.port, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("port")}
; - }, - }, - { - accessorKey: "username", - accessorFn: (row) => row.username, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("username")}
; - }, - }, - { - accessorKey: "createdAt", - accessorFn: (row) => row.createdAt, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("createdAt")}
; - }, - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - return ( - - - - - - Actions - - - - ); - }, - }, -]; diff --git a/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx b/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx deleted file mode 100644 index 95b4498ea..000000000 --- a/apps/dokploy/components/dashboard/swarm/servers/data-table.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client"; - -import { - type ColumnDef, - type ColumnFiltersState, - type SortingState, - type VisibilityState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { ChevronDown } from "lucide-react"; -import React from "react"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - isLoading: boolean; -} - -export function DataTable({ - columns, - data, - isLoading, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - return ( -
-
-
- - table.getColumn("Name")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- -
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
- {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/server-card.tsx b/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx similarity index 81% rename from apps/dokploy/components/dashboard/swarm/server-card.tsx rename to apps/dokploy/components/dashboard/swarm/servers/server-card.tsx index 100291144..4b732df4a 100644 --- a/apps/dokploy/components/dashboard/swarm/server-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx @@ -3,10 +3,7 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { AlertCircle, CheckCircle, HelpCircle, ServerIcon } from "lucide-react"; -import { useState } from "react"; -import { ShowContainers } from "../docker/show/show-containers"; -// import type { Server } from "../types/server"; -// import { ShowServerContainers } from "./ShowServerContainers"; +import { ShowContainers } from "../../docker/show/show-containers"; export interface Server { serverId: string; @@ -29,8 +26,6 @@ interface ServerOverviewCardProps { } export function ServerOverviewCard({ server }: ServerOverviewCardProps) { - const [showContainers, setShowContainers] = useState(false); - const getStatusIcon = (status: string) => { switch (status) { case "active": @@ -101,21 +96,7 @@ export function ServerOverviewCard({ server }: ServerOverviewCardProps) {
- {/* */}
- {/* {showContainers && ( -
- -
- )} */} ); diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx index 8768a88c8..a90546c9f 100644 --- a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx +++ b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx @@ -1,5 +1,5 @@ import { api } from "@/utils/api"; -import { ServerOverviewCard } from "../server-card"; +import { ServerOverviewCard } from "./server-card"; export default function ServersOverview() { const { data: servers, isLoading } = api.server.all.useQuery(); diff --git a/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx b/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx deleted file mode 100644 index 0486b1648..000000000 --- a/apps/dokploy/components/dashboard/swarm/servers/show-server.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { api } from "@/utils/api"; -import React from "react"; -import { columns } from "./columns"; -import { DataTable } from "./data-table"; - -function ShowApplicationServers() { - const { data, isLoading } = api.server.all.useQuery(); - - console.log(data); - - return ( - - ); -} - -export default ShowApplicationServers; diff --git a/apps/dokploy/components/dashboard/swarm/show/columns.tsx b/apps/dokploy/components/dashboard/swarm/show/columns.tsx deleted file mode 100644 index b07749363..000000000 --- a/apps/dokploy/components/dashboard/swarm/show/columns.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import type { ColumnDef } from "@tanstack/react-table"; -import { ArrowUpDown, MoreHorizontal } from "lucide-react"; -import * as React from "react"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -import { Badge } from "@/components/ui/badge"; -import ShowNodeApplications from "../applications/show-applications"; -import ShowContainers from "../containers/show-container"; -import { ShowNodeConfig } from "../details/show-node"; -// import { ShowContainerConfig } from "../config/show-container-config"; -// import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs"; -// import { DockerTerminalModal } from "../terminal/docker-terminal-modal"; -// import type { Container } from "./show-containers"; - -export interface SwarmList { - ID: string; - Hostname: string; - Availability: string; - EngineVersion: string; - Status: string; - ManagerStatus: string; - TLSStatus: string; -} - -export const columns: ColumnDef[] = [ - { - accessorKey: "ID", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("ID")}
; - }, - }, - { - accessorKey: "EngineVersion", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("EngineVersion")}
; - }, - }, - { - accessorKey: "Hostname", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return
{row.getValue("Hostname")}
; - }, - }, - // { - // accessorKey: "Status", - // header: ({ column }) => { - // return ( - // - // ); - // }, - // cell: ({ row }) => { - // const value = row.getValue("status") as string; - // return ( - //
- // - // {value} - // - //
- // ); - // }, - // }, - { - accessorKey: "Availability", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const value = row.getValue("Availability") as string; - return ( -
- - {value} - -
- ); - }, - }, - { - accessorKey: "ManagerStatus", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => ( -
{row.getValue("ManagerStatus")}
- ), - }, - { - id: "actions", - enableHiding: false, - cell: ({ row }) => { - return ( - - - - - - Actions - - - {/* - View Logs - - - - Terminal - */} - - - ); - }, - }, -]; diff --git a/apps/dokploy/components/dashboard/swarm/show/data-table.tsx b/apps/dokploy/components/dashboard/swarm/show/data-table.tsx deleted file mode 100644 index d3e993529..000000000 --- a/apps/dokploy/components/dashboard/swarm/show/data-table.tsx +++ /dev/null @@ -1,269 +0,0 @@ -"use client"; - -import { - type ColumnFiltersState, - type SortingState, - type VisibilityState, - type ColumnDef, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table"; - -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import React from "react"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, -} from "@/components/ui/dropdown-menu"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { ChevronDown } from "lucide-react"; -import { Input } from "@/components/ui/input"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - isLoading: boolean; -} - -export function DataTable({ - columns, - data, - isLoading, -}: DataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [] - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [rowSelection, setRowSelection] = React.useState({}); - - const table = useReactTable({ - data, - columns, - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - state: { - sorting, - columnFilters, - columnVisibility, - rowSelection, - }, - }); - - console.log("Data in DataTable", data); - - return ( -
-
-
- - table.getColumn("Hostname")?.setFilterValue(event.target.value) - } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
- {/* - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
*/} -
- {isLoading ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - - {isLoading ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
- )} -
-
- )} -
- {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx deleted file mode 100644 index e629654f7..000000000 --- a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { api } from "@/utils/api"; -import React from "react"; -import { columns } from "./columns"; -import { DataTable } from "./data-table"; - -function ShowSwarmNodes() { - const { data, isLoading } = api.swarm.getNodes.useQuery(); - - return ( - - ); -} - -export default ShowSwarmNodes; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 24fa43268..b294c09bc 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,6 +1,6 @@ import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; -import { ServerOverviewCard } from "@/components/dashboard/swarm/server-card"; +import { ServerOverviewCard } from "@/components/dashboard/swarm/servers/server-card"; import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; From b52f57cb0ddf09d37ff18efc8e5614374e7f1966 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 08:51:49 +0100 Subject: [PATCH 22/85] chore: remove imports --- .../swarm/details/{deatils-card.tsx => details-card.tsx} | 0 .../components/dashboard/swarm/monitoring-card.tsx | 2 +- apps/dokploy/pages/dashboard/swarm.tsx | 8 +------- 3 files changed, 2 insertions(+), 8 deletions(-) rename apps/dokploy/components/dashboard/swarm/details/{deatils-card.tsx => details-card.tsx} (100%) diff --git a/apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx similarity index 100% rename from apps/dokploy/components/dashboard/swarm/details/deatils-card.tsx rename to apps/dokploy/components/dashboard/swarm/details/details-card.tsx diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 7dae38de2..63984deca 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -15,7 +15,7 @@ import { Loader2, Server, } from "lucide-react"; -import { NodeCard } from "./details/deatils-card"; +import { NodeCard } from "./details/details-card"; export interface SwarmList { ID: string; diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index b294c09bc..ce4cfce2c 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,17 +1,11 @@ -import { ShowServers } from "@/components/dashboard/settings/servers/show-servers"; import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; -import { ServerOverviewCard } from "@/components/dashboard/swarm/servers/server-card"; import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; -import ShowApplicationServers from "@/components/dashboard/swarm/servers/show-server"; -import ShowSwarmNodes from "@/components/dashboard/swarm/show/show-nodes"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { Separator } from "@/components/ui/separator"; import { appRouter } from "@/server/api/root"; -import { api } from "@/utils/api"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; import type { GetServerSidePropsContext } from "next"; -import React, { type ReactElement } from "react"; +import type { ReactElement } from "react"; import superjson from "superjson"; const Dashboard = () => { From 8642d8235eb58abf530623cc130142f24c233317 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Wed, 18 Dec 2024 09:14:10 +0100 Subject: [PATCH 23/85] chore: add seperator and make tittles big --- .../dashboard/swarm/monitoring-card.tsx | 23 ++++---- .../swarm/servers/servers-overview.tsx | 56 ++++++++++++++++++- apps/dokploy/pages/dashboard/swarm.tsx | 12 ++-- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 63984deca..81a681721 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -1,4 +1,5 @@ import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tooltip, @@ -15,6 +16,7 @@ import { Loader2, Server, } from "lucide-react"; +import Link from "next/link"; import { NodeCard } from "./details/details-card"; export interface SwarmList { @@ -72,7 +74,6 @@ export default function SwarmMonitorCard() { ); } - console.log(nodes); const totalNodes = nodes.length; const activeNodesCount = nodes.filter( (node) => node.Status === "Ready", @@ -103,23 +104,21 @@ export default function SwarmMonitorCard() { return (
+
+

Docker Swarm Overview

+ +
Docker Swarm Monitor - {/* */}
diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx index a90546c9f..bd54f43ef 100644 --- a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx +++ b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx @@ -1,19 +1,69 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { api } from "@/utils/api"; +import { LoaderIcon } from "lucide-react"; import { ServerOverviewCard } from "./server-card"; export default function ServersOverview() { const { data: servers, isLoading } = api.server.all.useQuery(); if (isLoading) { - return
Loading...
; + return ( + <> + + + + + + + + + + + + +
+
+ IP Address: +
+
+ Port: +
+
+ Username: +
+
+ App Name: +
+
+ Docker Cleanup: +
+
+ Created At: +
+
+
+
+ + ); } if (!servers) { return
No servers found
; } return ( -
-

Server Overview

+
+
+

Server Overview

+ +
{servers.map((server) => ( diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index ce4cfce2c..d8b6b0615 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,6 +1,7 @@ import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { Separator } from "@/components/ui/separator"; import { appRouter } from "@/server/api/root"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; @@ -14,13 +15,10 @@ const Dashboard = () => {
- - {/* */} - {/*

Swarm Nodes

- - -

Server Nodes

- */} + +
+ +
); }; From 6211a19805dcfa71c614609111563cc6500ab30e Mon Sep 17 00:00:00 2001 From: faytranevozter Date: Wed, 18 Dec 2024 16:40:33 +0700 Subject: [PATCH 24/85] feat: add support for viewing docker logs in swarm mode --- .../dashboard/application/logs/show.tsx | 6 +- .../dashboard/compose/logs/show-stack.tsx | 100 +++++++++++++++ .../dashboard/compose/logs/show.tsx | 1 + .../dashboard/docker/logs/docker-logs-id.tsx | 8 +- .../docker/logs/show-docker-modal-logs.tsx | 6 +- .../settings/web-server/show-modal-logs.tsx | 6 +- .../services/compose/[composeId].tsx | 18 ++- apps/dokploy/server/api/routers/docker.ts | 24 ++++ .../server/wss/docker-container-logs.ts | 5 +- packages/server/src/services/docker.ts | 118 ++++++++++++++++++ 10 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 apps/dokploy/components/dashboard/compose/logs/show-stack.tsx diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index dba3666c7..d78beafc0 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -35,7 +35,7 @@ interface Props { } export const ShowDockerLogs = ({ appName, serverId }: Props) => { - const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery( + const { data, isLoading } = api.docker.getServiceContainersByAppName.useQuery( { appName, serverId, @@ -81,7 +81,8 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => { key={container.containerId} value={container.containerId} > - {container.name} ({container.containerId}) {container.state} + {container.name} ({container.containerId}@{container.node}){" "} + {container.state} ))} Containers ({data?.length}) @@ -91,6 +92,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => { diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx new file mode 100644 index 000000000..4bfc12d9e --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -0,0 +1,100 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { api } from "@/utils/api"; +import { Loader, Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; +export const DockerLogs = dynamic( + () => + import("@/components/dashboard/docker/logs/docker-logs-id").then( + (e) => e.DockerLogsId, + ), + { + ssr: false, + }, +); + +interface Props { + appName: string; + serverId?: string; +} + +export const ShowDockerLogsStack = ({ appName, serverId }: Props) => { + const { data, isLoading } = api.docker.getStackContainersByAppName.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName, + }, + ); + const [containerId, setContainerId] = useState(); + + useEffect(() => { + if (data && data?.length > 0) { + setContainerId(data[0]?.containerId); + } + }, [data]); + + return ( + + + Logs + + Watch the logs of the application in real time + + + + + + + + + + ); +}; diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 6b39f4137..bf7e2993e 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -97,6 +97,7 @@ export const ShowDockerLogsCompose = ({ diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 3f30c292c..1fd8cea48 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -12,6 +12,7 @@ import { type LogLine, getLogType, parseLogs } from "./utils"; interface Props { containerId: string; serverId?: string | null; + runType: "swarm" | "native"; } export const priorities = [ @@ -37,7 +38,11 @@ export const priorities = [ }, ]; -export const DockerLogsId: React.FC = ({ containerId, serverId }) => { +export const DockerLogsId: React.FC = ({ + containerId, + serverId, + runType, +}) => { const { data } = api.docker.getConfig.useQuery( { containerId, @@ -104,6 +109,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { tail: lines.toString(), since, search, + runType, }); if (serverId) { diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx index f8531d774..619b25d0c 100644 --- a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx @@ -46,7 +46,11 @@ export const ShowDockerModalLogs = ({ View the logs for {containerId}
- +
diff --git a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx index 92401dc35..12e7b6704 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/show-modal-logs.tsx @@ -91,7 +91,11 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => { - +
diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index a4cb855cc..6b828dc7d 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -6,6 +6,7 @@ import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show- import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show"; import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"; import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show"; +import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack"; import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show"; import { UpdateCompose } from "@/components/dashboard/compose/update-compose"; import { ProjectLayout } from "@/components/layouts/project-layout"; @@ -251,11 +252,18 @@ const Service = (
- + {data?.composeType === "docker-compose" ? ( + + ) : ( + + )}
diff --git a/apps/dokploy/server/api/routers/docker.ts b/apps/dokploy/server/api/routers/docker.ts index cb6b2712e..f6972e16b 100644 --- a/apps/dokploy/server/api/routers/docker.ts +++ b/apps/dokploy/server/api/routers/docker.ts @@ -4,6 +4,8 @@ import { getContainers, getContainersByAppLabel, getContainersByAppNameMatch, + getServiceContainersByAppName, + getStackContainersByAppName, } from "@dokploy/server"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; @@ -68,4 +70,26 @@ export const dockerRouter = createTRPCRouter({ .query(async ({ input }) => { return await getContainersByAppLabel(input.appName, input.serverId); }), + + getStackContainersByAppName: protectedProcedure + .input( + z.object({ + appName: z.string().min(1), + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getStackContainersByAppName(input.appName, input.serverId); + }), + + getServiceContainersByAppName: protectedProcedure + .input( + z.object({ + appName: z.string().min(1), + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getServiceContainersByAppName(input.appName, input.serverId); + }), }); diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 0c1e66a4d..c1049ee0c 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -34,6 +34,7 @@ export const setupDockerContainerLogsWebSocketServer = ( const search = url.searchParams.get("search"); const since = url.searchParams.get("since"); const serverId = url.searchParams.get("serverId"); + const runType = url.searchParams.get("runType"); const { user, session } = await validateWebSocketRequest(req); if (!containerId) { @@ -53,7 +54,7 @@ export const setupDockerContainerLogsWebSocketServer = ( const client = new Client(); client .once("ready", () => { - const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + const baseCommand = `docker ${runType==="swarm"?"service":"container"} logs --timestamps --tail ${tail} ${ since === "all" ? "" : `--since ${since}` } --follow ${containerId}`; const escapedSearch = search ? search.replace(/'/g, "'\\''") : ""; @@ -97,7 +98,7 @@ export const setupDockerContainerLogsWebSocketServer = ( }); } else { const shell = getShell(); - const baseCommand = `docker container logs --timestamps --tail ${tail} ${ + const baseCommand = `docker ${runType==="swarm"?"service":"container"} logs --timestamps --tail ${tail} ${ since === "all" ? "" : `--since ${since}` } --follow ${containerId}`; const command = search diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 6ac613542..7b55b5e9e 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -157,6 +157,124 @@ export const getContainersByAppNameMatch = async ( return []; }; +export const getStackContainersByAppName = async ( + appName: string, + serverId?: string, +) => { + try { + let result: string[] = []; + + const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`; + if (serverId) { + const { stdout, stderr } = await execAsyncRemote(serverId, command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + result = stdout.trim().split("\n"); + } else { + const { stdout, stderr } = await execAsync(command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + + result = stdout.trim().split("\n"); + } + + const containers = result.map((line) => { + const parts = line.split(" | "); + const containerId = parts[0] + ? parts[0].replace("CONTAINER ID : ", "").trim() + : "No container id"; + const name = parts[1] + ? parts[1].replace("Name: ", "").trim() + : "No container name"; + + const state = parts[2] + ? parts[2].replace("State: ", "").trim() + : "No state"; + const node = parts[3] + ? parts[3].replace("Node: ", "").trim() + : "No specific node"; + return { + containerId, + name, + state, + node, + }; + }); + + return containers || []; + } catch (error) {} + + return []; +}; + +export const getServiceContainersByAppName = async ( + appName: string, + serverId?: string, +) => { + try { + let result: string[] = []; + + const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`; + + if (serverId) { + const { stdout, stderr } = await execAsyncRemote(serverId, command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + result = stdout.trim().split("\n"); + } else { + const { stdout, stderr } = await execAsync(command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + + result = stdout.trim().split("\n"); + } + + const containers = result.map((line) => { + const parts = line.split(" | "); + const containerId = parts[0] + ? parts[0].replace("CONTAINER ID : ", "").trim() + : "No container id"; + const name = parts[1] + ? parts[1].replace("Name: ", "").trim() + : "No container name"; + + const state = parts[2] + ? parts[2].replace("State: ", "").trim() + : "No state"; + + const node = parts[3] + ? parts[3].replace("Node: ", "").trim() + : "No specific node"; + return { + containerId, + name, + state, + node, + }; + }); + + return containers || []; + } catch (error) {} + + return []; +}; + export const getContainersByAppLabel = async ( appName: string, serverId?: string, From 8ea453f4440f39226b5e0d42684aee5baf50c7c7 Mon Sep 17 00:00:00 2001 From: DJKnaeckebrot Date: Wed, 18 Dec 2024 13:01:09 +0100 Subject: [PATCH 25/85] feat: add application handling --- .../application/delete-application.tsx | 25 + .../dokploy/server/api/routers/application.ts | 9 +- packages/server/src/db/schema/application.ts | 7 +- packages/server/src/utils/docker/utils.ts | 850 +++++++++--------- 4 files changed, 467 insertions(+), 424 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/delete-application.tsx b/apps/dokploy/components/dashboard/application/delete-application.tsx index f34d29a78..ff63ef5c5 100644 --- a/apps/dokploy/components/dashboard/application/delete-application.tsx +++ b/apps/dokploy/components/dashboard/application/delete-application.tsx @@ -1,5 +1,6 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -31,6 +32,7 @@ const deleteApplicationSchema = z.object({ projectName: z.string().min(1, { message: "Application name is required", }), + deleteVolumes: z.boolean(), }); type DeleteApplication = z.infer; @@ -50,6 +52,7 @@ export const DeleteApplication = ({ applicationId }: Props) => { const form = useForm({ defaultValues: { projectName: "", + deleteVolumes: false, }, resolver: zodResolver(deleteApplicationSchema), }); @@ -59,6 +62,7 @@ export const DeleteApplication = ({ applicationId }: Props) => { if (formData.projectName === expectedName) { await mutateAsync({ applicationId, + deleteVolumes: formData.deleteVolumes, }) .then((data) => { push(`/dashboard/project/${data?.projectId}`); @@ -134,6 +138,27 @@ export const DeleteApplication = ({ applicationId }: Props) => { )} /> + ( + +
+ + + + + + Delete volumes associated with this compose + +
+ +
+ )} + />
diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 2902c8eda..9b16d5796 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -6,6 +6,7 @@ import { import { db } from "@/server/db"; import { apiCreateApplication, + apiDeleteApplication, apiFindMonitoringStats, apiFindOneApplication, apiReloadApplication, @@ -142,7 +143,7 @@ export const applicationRouter = createTRPCRouter({ }), delete: protectedProcedure - .input(apiFindOneApplication) + .input(apiDeleteApplication) .mutation(async ({ input, ctx }) => { if (ctx.user.rol === "user") { await checkServiceAccess( @@ -178,7 +179,11 @@ export const applicationRouter = createTRPCRouter({ async () => await removeTraefikConfig(application.appName, application.serverId), async () => - await removeService(application?.appName, application.serverId), + await removeService( + application?.appName, + application.serverId, + input.deleteVolumes, + ), ]; for (const operation of cleanupOperations) { diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index d9b1a5df4..923ea130e 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -17,6 +17,7 @@ import { github } from "./github"; import { gitlab } from "./gitlab"; import { mounts } from "./mount"; import { ports } from "./port"; +import { previewDeployments } from "./preview-deployments"; import { projects } from "./project"; import { redirects } from "./redirects"; import { registry } from "./registry"; @@ -25,7 +26,6 @@ import { server } from "./server"; import { applicationStatus, certificateType } from "./shared"; import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; -import { previewDeployments } from "./preview-deployments"; export const sourceType = pgEnum("sourceType", [ "docker", @@ -518,3 +518,8 @@ export const apiUpdateApplication = createSchema applicationId: z.string().min(1), }) .omit({ serverId: true }); + +export const apiDeleteApplication = z.object({ + applicationId: z.string().min(1), + deleteVolumes: z.boolean(), +}); diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 216ee8671..e8c9e6c23 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -15,520 +15,528 @@ import { spawnAsync } from "../process/spawnAsync"; import { getRemoteDocker } from "../servers/remote-docker"; interface RegistryAuth { - username: string; - password: string; - registryUrl: string; + username: string; + password: string; + registryUrl: string; } export const pullImage = async ( - dockerImage: string, - onData?: (data: any) => void, - authConfig?: Partial, + dockerImage: string, + onData?: (data: any) => void, + authConfig?: Partial ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } - - if (authConfig?.username && authConfig?.password) { - await spawnAsync( - "docker", - [ - "login", - authConfig.registryUrl || "", - "-u", - authConfig.username, - "-p", - authConfig.password, - ], - onData, - ); - } - await spawnAsync("docker", ["pull", dockerImage], onData); - } catch (error) { - throw error; - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } + + if (authConfig?.username && authConfig?.password) { + await spawnAsync( + "docker", + [ + "login", + authConfig.registryUrl || "", + "-u", + authConfig.username, + "-p", + authConfig.password, + ], + onData + ); + } + await spawnAsync("docker", ["pull", dockerImage], onData); + } catch (error) { + throw error; + } }; export const pullRemoteImage = async ( - dockerImage: string, - serverId: string, - onData?: (data: any) => void, - authConfig?: Partial, + dockerImage: string, + serverId: string, + onData?: (data: any) => void, + authConfig?: Partial ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } - - const remoteDocker = await getRemoteDocker(serverId); - - await new Promise((resolve, reject) => { - remoteDocker.pull( - dockerImage, - { authconfig: authConfig }, - (err, stream) => { - if (err) { - reject(err); - return; - } - - remoteDocker.modem.followProgress( - stream as Readable, - (err: Error | null, res) => { - if (!err) { - resolve(res); - } - if (err) { - reject(err); - } - }, - (event) => { - onData?.(event); - }, - ); - }, - ); - }); - } catch (error) { - throw error; - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } + + const remoteDocker = await getRemoteDocker(serverId); + + await new Promise((resolve, reject) => { + remoteDocker.pull( + dockerImage, + { authconfig: authConfig }, + (err, stream) => { + if (err) { + reject(err); + return; + } + + remoteDocker.modem.followProgress( + stream as Readable, + (err: Error | null, res) => { + if (!err) { + resolve(res); + } + if (err) { + reject(err); + } + }, + (event) => { + onData?.(event); + } + ); + } + ); + }); + } catch (error) { + throw error; + } }; export const containerExists = async (containerName: string) => { - const container = docker.getContainer(containerName); - try { - await container.inspect(); - return true; - } catch (error) { - return false; - } + const container = docker.getContainer(containerName); + try { + await container.inspect(); + return true; + } catch (error) { + return false; + } }; export const stopService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsync(`docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const stopServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const getContainerByName = (name: string): Promise => { - const opts = { - limit: 1, - filters: { - name: [name], - }, - }; - return new Promise((resolve, reject) => { - docker.listContainers(opts, (err, containers) => { - if (err) { - reject(err); - } else if (containers?.length === 0) { - reject(new Error(`No container found with name: ${name}`)); - } else if (containers && containers?.length > 0 && containers[0]) { - resolve(containers[0]); - } - }); - }); + const opts = { + limit: 1, + filters: { + name: [name], + }, + }; + return new Promise((resolve, reject) => { + docker.listContainers(opts, (err, containers) => { + if (err) { + reject(err); + } else if (containers?.length === 0) { + reject(new Error(`No container found with name: ${name}`)); + } else if (containers && containers?.length > 0 && containers[0]) { + resolve(containers[0]); + } + }); + }); }; export const cleanUpUnusedImages = async (serverId?: string) => { - try { - if (serverId) { - await execAsyncRemote(serverId, "docker image prune --all --force"); - } else { - await execAsync("docker image prune --all --force"); - } - } catch (error) { - console.error(error); - throw error; - } + try { + if (serverId) { + await execAsyncRemote(serverId, "docker image prune --all --force"); + } else { + await execAsync("docker image prune --all --force"); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanStoppedContainers = async (serverId?: string) => { - try { - if (serverId) { - await execAsyncRemote(serverId, "docker container prune --force"); - } else { - await execAsync("docker container prune --force"); - } - } catch (error) { - console.error(error); - throw error; - } + try { + if (serverId) { + await execAsyncRemote(serverId, "docker container prune --force"); + } else { + await execAsync("docker container prune --force"); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpUnusedVolumes = async (serverId?: string) => { - try { - if (serverId) { - await execAsyncRemote(serverId, "docker volume prune --all --force"); - } else { - await execAsync("docker volume prune --all --force"); - } - } catch (error) { - console.error(error); - throw error; - } + try { + if (serverId) { + await execAsyncRemote(serverId, "docker volume prune --all --force"); + } else { + await execAsync("docker volume prune --all --force"); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpInactiveContainers = async () => { - try { - const containers = await docker.listContainers({ all: true }); - const inactiveContainers = containers.filter( - (container) => container.State !== "running", - ); - - for (const container of inactiveContainers) { - await docker.getContainer(container.Id).remove({ force: true }); - console.log(`Cleaning up inactive container: ${container.Id}`); - } - } catch (error) { - console.error("Error cleaning up inactive containers:", error); - throw error; - } + try { + const containers = await docker.listContainers({ all: true }); + const inactiveContainers = containers.filter( + (container) => container.State !== "running" + ); + + for (const container of inactiveContainers) { + await docker.getContainer(container.Id).remove({ force: true }); + console.log(`Cleaning up inactive container: ${container.Id}`); + } + } catch (error) { + console.error("Error cleaning up inactive containers:", error); + throw error; + } }; export const cleanUpDockerBuilder = async (serverId?: string) => { - if (serverId) { - await execAsyncRemote(serverId, "docker builder prune --all --force"); - } else { - await execAsync("docker builder prune --all --force"); - } + if (serverId) { + await execAsyncRemote(serverId, "docker builder prune --all --force"); + } else { + await execAsync("docker builder prune --all --force"); + } }; export const cleanUpSystemPrune = async (serverId?: string) => { - if (serverId) { - await execAsyncRemote( - serverId, - "docker system prune --all --force --volumes", - ); - } else { - await execAsync("docker system prune --all --force --volumes"); - } + if (serverId) { + await execAsyncRemote( + serverId, + "docker system prune --all --force --volumes" + ); + } else { + await execAsync("docker system prune --all --force --volumes"); + } }; export const startService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsync(`docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const startServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const removeService = async ( - appName: string, - serverId?: string | null, + appName: string, + serverId?: string | null, + deleteVolumes = false ) => { - try { - const command = `docker service rm ${appName}`; - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - return error; - } + try { + let command: string; + + if (deleteVolumes) { + command = `docker service rm --force ${appName}`; + } else { + command = `docker service rm ${appName}`; + } + + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + return error; + } }; export const prepareEnvironmentVariables = ( - serviceEnv: string | null, - projectEnv?: string | null, + serviceEnv: string | null, + projectEnv?: string | null ) => { - const projectVars = parse(projectEnv ?? ""); - const serviceVars = parse(serviceEnv ?? ""); - - const resolvedVars = Object.entries(serviceVars).map(([key, value]) => { - let resolvedValue = value; - if (projectVars) { - resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => { - if (projectVars[ref] !== undefined) { - return projectVars[ref]; - } - throw new Error(`Invalid project environment variable: project.${ref}`); - }); - } - return `${key}=${resolvedValue}`; - }); - - return resolvedVars; + const projectVars = parse(projectEnv ?? ""); + const serviceVars = parse(serviceEnv ?? ""); + + const resolvedVars = Object.entries(serviceVars).map(([key, value]) => { + let resolvedValue = value; + if (projectVars) { + resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => { + if (projectVars[ref] !== undefined) { + return projectVars[ref]; + } + throw new Error(`Invalid project environment variable: project.${ref}`); + }); + } + return `${key}=${resolvedValue}`; + }); + + return resolvedVars; }; export const prepareBuildArgs = (input: string | null) => { - const pairs = (input ?? "").split("\n"); + const pairs = (input ?? "").split("\n"); - const jsonObject: Record = {}; + const jsonObject: Record = {}; - for (const pair of pairs) { - const [key, value] = pair.split("="); - if (key && value) { - jsonObject[key] = value; - } - } + for (const pair of pairs) { + const [key, value] = pair.split("="); + if (key && value) { + jsonObject[key] = value; + } + } - return jsonObject; + return jsonObject; }; export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } - - return mounts - .filter((mount) => mount.type === "volume") - .map((mount) => ({ - Type: "volume" as const, - Source: mount.volumeName || "", - Target: mount.mountPath, - })); + if (!mounts || mounts.length === 0) { + return []; + } + + return mounts + .filter((mount) => mount.type === "volume") + .map((mount) => ({ + Type: "volume" as const, + Source: mount.volumeName || "", + Target: mount.mountPath, + })); }; type Resources = { - memoryLimit: number | null; - memoryReservation: number | null; - cpuLimit: number | null; - cpuReservation: number | null; + memoryLimit: number | null; + memoryReservation: number | null; + cpuLimit: number | null; + cpuReservation: number | null; }; export const calculateResources = ({ - memoryLimit, - memoryReservation, - cpuLimit, - cpuReservation, + memoryLimit, + memoryReservation, + cpuLimit, + cpuReservation, }: Resources): ResourceRequirements => { - return { - Limits: { - MemoryBytes: memoryLimit ?? undefined, - NanoCPUs: cpuLimit ?? undefined, - }, - Reservations: { - MemoryBytes: memoryReservation ?? undefined, - NanoCPUs: cpuReservation ?? undefined, - }, - }; + return { + Limits: { + MemoryBytes: memoryLimit ?? undefined, + NanoCPUs: cpuLimit ?? undefined, + }, + Reservations: { + MemoryBytes: memoryReservation ?? undefined, + NanoCPUs: cpuReservation ?? undefined, + }, + }; }; export const generateConfigContainer = (application: ApplicationNested) => { - const { - healthCheckSwarm, - restartPolicySwarm, - placementSwarm, - updateConfigSwarm, - rollbackConfigSwarm, - modeSwarm, - labelsSwarm, - replicas, - mounts, - networkSwarm, - } = application; - - const haveMounts = mounts.length > 0; - - return { - ...(healthCheckSwarm && { - HealthCheck: healthCheckSwarm, - }), - ...(restartPolicySwarm - ? { - RestartPolicy: restartPolicySwarm, - } - : {}), - ...(placementSwarm - ? { - Placement: placementSwarm, - } - : { - // if app have mounts keep manager as constraint - Placement: { - Constraints: haveMounts ? ["node.role==manager"] : [], - }, - }), - ...(labelsSwarm && { - Labels: labelsSwarm, - }), - ...(modeSwarm - ? { - Mode: modeSwarm, - } - : { - // use replicas value if no modeSwarm provided - Mode: { - Replicated: { - Replicas: replicas, - }, - }, - }), - ...(rollbackConfigSwarm && { - RollbackConfig: rollbackConfigSwarm, - }), - ...(updateConfigSwarm - ? { UpdateConfig: updateConfigSwarm } - : { - // default config if no updateConfigSwarm provided - UpdateConfig: { - Parallelism: 1, - Order: "start-first", - }, - }), - ...(networkSwarm - ? { - Networks: networkSwarm, - } - : { - Networks: [{ Target: "dokploy-network" }], - }), - }; + const { + healthCheckSwarm, + restartPolicySwarm, + placementSwarm, + updateConfigSwarm, + rollbackConfigSwarm, + modeSwarm, + labelsSwarm, + replicas, + mounts, + networkSwarm, + } = application; + + const haveMounts = mounts.length > 0; + + return { + ...(healthCheckSwarm && { + HealthCheck: healthCheckSwarm, + }), + ...(restartPolicySwarm + ? { + RestartPolicy: restartPolicySwarm, + } + : {}), + ...(placementSwarm + ? { + Placement: placementSwarm, + } + : { + // if app have mounts keep manager as constraint + Placement: { + Constraints: haveMounts ? ["node.role==manager"] : [], + }, + }), + ...(labelsSwarm && { + Labels: labelsSwarm, + }), + ...(modeSwarm + ? { + Mode: modeSwarm, + } + : { + // use replicas value if no modeSwarm provided + Mode: { + Replicated: { + Replicas: replicas, + }, + }, + }), + ...(rollbackConfigSwarm && { + RollbackConfig: rollbackConfigSwarm, + }), + ...(updateConfigSwarm + ? { UpdateConfig: updateConfigSwarm } + : { + // default config if no updateConfigSwarm provided + UpdateConfig: { + Parallelism: 1, + Order: "start-first", + }, + }), + ...(networkSwarm + ? { + Networks: networkSwarm, + } + : { + Networks: [{ Target: "dokploy-network" }], + }), + }; }; export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } - - return mounts - .filter((mount) => mount.type === "bind") - .map((mount) => ({ - Type: "bind" as const, - Source: mount.hostPath || "", - Target: mount.mountPath, - })); + if (!mounts || mounts.length === 0) { + return []; + } + + return mounts + .filter((mount) => mount.type === "bind") + .map((mount) => ({ + Type: "bind" as const, + Source: mount.hostPath || "", + Target: mount.mountPath, + })); }; export const generateFileMounts = ( - appName: string, - service: - | ApplicationNested - | MongoNested - | MariadbNested - | MysqlNested - | PostgresNested - | RedisNested, + appName: string, + service: + | ApplicationNested + | MongoNested + | MariadbNested + | MysqlNested + | PostgresNested + | RedisNested ) => { - const { mounts } = service; - const { APPLICATIONS_PATH } = paths(!!service.serverId); - if (!mounts || mounts.length === 0) { - return []; - } - - return mounts - .filter((mount) => mount.type === "file") - .map((mount) => { - const fileName = mount.filePath; - const absoluteBasePath = path.resolve(APPLICATIONS_PATH); - const directory = path.join(absoluteBasePath, appName, "files"); - const sourcePath = path.join(directory, fileName || ""); - return { - Type: "bind" as const, - Source: sourcePath, - Target: mount.mountPath, - }; - }); + const { mounts } = service; + const { APPLICATIONS_PATH } = paths(!!service.serverId); + if (!mounts || mounts.length === 0) { + return []; + } + + return mounts + .filter((mount) => mount.type === "file") + .map((mount) => { + const fileName = mount.filePath; + const absoluteBasePath = path.resolve(APPLICATIONS_PATH); + const directory = path.join(absoluteBasePath, appName, "files"); + const sourcePath = path.join(directory, fileName || ""); + return { + Type: "bind" as const, + Source: sourcePath, + Target: mount.mountPath, + }; + }); }; export const createFile = async ( - outputPath: string, - filePath: string, - content: string, + outputPath: string, + filePath: string, + content: string ) => { - try { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - fs.mkdirSync(fullPath, { recursive: true }); - return; - } - - const directory = path.dirname(fullPath); - fs.mkdirSync(directory, { recursive: true }); - fs.writeFileSync(fullPath, content || ""); - } catch (error) { - throw error; - } + try { + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + fs.mkdirSync(fullPath, { recursive: true }); + return; + } + + const directory = path.dirname(fullPath); + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(fullPath, content || ""); + } catch (error) { + throw error; + } }; export const encodeBase64 = (content: string) => - Buffer.from(content, "utf-8").toString("base64"); + Buffer.from(content, "utf-8").toString("base64"); export const getCreateFileCommand = ( - outputPath: string, - filePath: string, - content: string, + outputPath: string, + filePath: string, + content: string ) => { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - return `mkdir -p ${fullPath};`; - } - - const directory = path.dirname(fullPath); - const encodedContent = encodeBase64(content); - return ` + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + return `mkdir -p ${fullPath};`; + } + + const directory = path.dirname(fullPath); + const encodedContent = encodeBase64(content); + return ` mkdir -p ${directory}; echo "${encodedContent}" | base64 -d > "${fullPath}"; `; }; export const getServiceContainer = async (appName: string) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; - - const containers = await docker.listContainers({ - filters: JSON.stringify(filter), - }); - - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } - - const container = containers[0]; - - return container; - } catch (error) { - throw error; - } + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; + + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); + + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } + + const container = containers[0]; + + return container; + } catch (error) { + throw error; + } }; export const getRemoteServiceContainer = async ( - serverId: string, - appName: string, + serverId: string, + appName: string ) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; - const remoteDocker = await getRemoteDocker(serverId); - const containers = await remoteDocker.listContainers({ - filters: JSON.stringify(filter), - }); - - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } - - const container = containers[0]; - - return container; - } catch (error) { - throw error; - } + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; + const remoteDocker = await getRemoteDocker(serverId); + const containers = await remoteDocker.listContainers({ + filters: JSON.stringify(filter), + }); + + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } + + const container = containers[0]; + + return container; + } catch (error) { + throw error; + } }; From 06b8c824844bd543138f7d68ab47e8e7adfa1aac Mon Sep 17 00:00:00 2001 From: Shadow Date: Wed, 18 Dec 2024 11:50:30 -0600 Subject: [PATCH 26/85] feat: add a toggle for replica sets to be used or not --- .../dashboard/project/add-database.tsx | 27 ++++++++++ packages/server/src/db/schema/mongo.ts | 5 +- packages/server/src/utils/databases/mongo.ts | 50 +++++++++++-------- 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index aaf4940b0..98e3019a4 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -35,6 +35,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { slugify } from "@/lib/slug"; import { api } from "@/utils/api"; @@ -95,6 +96,7 @@ const mySchema = z.discriminatedUnion("type", [ .object({ type: z.literal("mongo"), databaseUser: z.string().default("mongo"), + replicaSets: z.boolean().default(false), }) .merge(baseDatabaseSchema), z @@ -216,6 +218,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], serverId: data.serverId, + replicaSets: data.replicaSets, }); } else if (data.type === "redis") { promise = redisMutation.mutateAsync({ @@ -540,6 +543,30 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { ); }} /> + + {type === "mongo" && ( + { + return ( + + Use Replica Sets + + + + + + + ); + }} + /> + )}
diff --git a/packages/server/src/db/schema/mongo.ts b/packages/server/src/db/schema/mongo.ts index 757ba9c90..73f297b7f 100644 --- a/packages/server/src/db/schema/mongo.ts +++ b/packages/server/src/db/schema/mongo.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm"; -import { integer, pgTable, text } from "drizzle-orm/pg-core"; +import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; @@ -43,6 +43,7 @@ export const mongo = pgTable("mongo", { serverId: text("serverId").references(() => server.serverId, { onDelete: "cascade", }), + replicaSets: boolean("replicaSets").default(false), }); export const mongoRelations = relations(mongo, ({ one, many }) => ({ @@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mongo, { externalPort: z.number(), description: z.string().optional(), serverId: z.string().optional(), + replicaSets: z.boolean().default(false), }); export const apiCreateMongo = createSchema @@ -89,6 +91,7 @@ export const apiCreateMongo = createSchema databaseUser: true, databasePassword: true, serverId: true, + replicaSets: true, }) .required(); diff --git a/packages/server/src/utils/databases/mongo.ts b/packages/server/src/utils/databases/mongo.ts index 3c9556f9c..2cce315c4 100644 --- a/packages/server/src/utils/databases/mongo.ts +++ b/packages/server/src/utils/databases/mongo.ts @@ -28,48 +28,56 @@ export const buildMongo = async (mongo: MongoNested) => { databasePassword, command, mounts, + replicaSets, } = mongo; - const initReplicaSet = ` + const startupScript = ` #!/bin/bash +${ + replicaSets + ? ` mongod --port 27017 --replSet rs0 --bind_ip_all & MONGOD_PID=$! # Wait for MongoDB to be ready while ! mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; do - sleep 2 + sleep 2 done # Check if replica set is already initialized REPLICA_STATUS=$(mongosh --quiet --eval "rs.status().ok || 0") if [ "$REPLICA_STATUS" != "1" ]; then - echo "Initializing replica set..." - mongosh --eval ' - rs.initiate({ - _id: "rs0", - members: [{ _id: 0, host: "localhost:27017", priority: 1 }] - }); + echo "Initializing replica set..." + mongosh --eval ' + rs.initiate({ + _id: "rs0", + members: [{ _id: 0, host: "localhost:27017", priority: 1 }] + }); // Wait for the replica set to initialize - while (!rs.isMaster().ismaster) { - sleep(1000); - } + while (!rs.isMaster().ismaster) { + sleep(1000); + } // Create root user after replica set is initialized and we are primary - db.getSiblingDB("admin").createUser({ - user: "${databaseUser}", - pwd: "${databasePassword}", - roles: ["root"] - }); - ' + db.getSiblingDB("admin").createUser({ + user: "${databaseUser}", + pwd: "${databasePassword}", + roles: ["root"] + }); + ' + else - echo "Replica set already initialized." + echo "Replica set already initialized." fi +` + : "mongod --port 27017 --bind_ip_all & MONGOD_PID=$!" +} -wait $MONGOD_PID`; +${command ?? "wait $MONGOD_PID"}`; - const defaultMongoEnv = `MONGO_INITDB_DATABASE=admin\n${env ? `${env}` : ""}`; + const defaultMongoEnv = `MONGO_INITDB_ROOT_USERNAME=${databaseUser}\nMONGO_INITDB_ROOT_PASSWORD=${databasePassword}\nMONGO_INITDB_DATABASE=admin\n${env ? `${env}` : ""}`; const resources = calculateResources({ memoryLimit, @@ -96,7 +104,7 @@ wait $MONGOD_PID`; Env: envVariables, Mounts: [...volumesMount, ...bindsMount, ...filesMount], Command: ["/bin/bash"], - Args: ["-c", command ?? initReplicaSet], + Args: ["-c", startupScript], }, Networks: [{ Target: "dokploy-network" }], Resources: { From 3858205e520c44a24c912f58565c50b009d45d9f Mon Sep 17 00:00:00 2001 From: 190km Date: Wed, 18 Dec 2024 22:36:57 +0100 Subject: [PATCH 27/85] style: better preview deployment card --- .../preview-deployment-card.tsx | 141 +++++++++++++ .../show-preview-builds.tsx | 2 +- .../show-preview-deployments.tsx | 185 ++++++------------ 3 files changed, 197 insertions(+), 131 deletions(-) create mode 100644 apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx new file mode 100644 index 000000000..c5b381eb1 --- /dev/null +++ b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx @@ -0,0 +1,141 @@ +import { StatusTooltip } from "@/components/shared/status-tooltip"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { Clock, GitBranch, GitPullRequest, Pencil } from "lucide-react"; +import Link from "next/link"; +import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { ShowPreviewBuilds } from "./show-preview-builds"; +import { RouterOutputs } from "@/utils/api"; +import { AddPreviewDomain } from "./add-preview-domain"; +import { DateTooltip } from "@/components/shared/date-tooltip"; + +interface PreviewDeploymentCardProps { + appName: string; + serverId: string; + onDeploymentDelete: (deploymentId: string) => void; + deploymentId: string; + deploymentUrl: string; + deployments: RouterOutputs["deployment"]["all"]; + + domainId: string; + domainHost: string; + + pullRequestTitle: string; + pullRequestUrl: string; + status: "running" | "error" | "done" | "idle" | undefined | null; + branch: string; + date: string; + isLoading: boolean; +} + +export function PreviewDeploymentCard({ + appName, + serverId, + + onDeploymentDelete, + deploymentId, + deployments, + + domainId, + domainHost, + + pullRequestTitle, + pullRequestUrl, + isLoading, + status, + branch, + date, +}: PreviewDeploymentCardProps) { + return ( +
+
+ {pullRequestTitle} + + + {status + ?.replace("running", "Running") + .replace("done", "Done") + .replace("error", "Error") + .replace("idle", "Idle") || "Idle"} + +
+
+
+
+ + {domainHost} + +
+ + + +
+
+
+ + Branch: + + {" "} + {branch} + +
+
+ + Deployed: + + + +
+
+ +
+

Pull Request

+
+ + + {pullRequestTitle} + +
+
+
+
+
+ + + + + + + onDeploymentDelete(deploymentId)} + > + + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx index 4eb2107f6..bff6c9290 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx @@ -26,7 +26,7 @@ export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => { return ( - + diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 45451e78f..7be497ed7 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -8,26 +8,18 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; -import { Pencil, RocketIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import { RocketIcon } from "lucide-react"; +import React, { useState } from "react"; import { toast } from "sonner"; -import { ShowDeployment } from "../deployments/show-deployment"; -import Link from "next/link"; -import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; -import { DialogAction } from "@/components/shared/dialog-action"; -import { AddPreviewDomain } from "./add-preview-domain"; -import { GithubIcon } from "@/components/icons/data-tools-icons"; +import { PreviewDeploymentCard } from "./preview-deployment-card"; import { ShowPreviewSettings } from "./show-preview-settings"; -import { ShowPreviewBuilds } from "./show-preview-builds"; interface Props { applicationId: string; } export const ShowPreviewDeployments = ({ applicationId }: Props) => { - const [activeLog, setActiveLog] = useState(null); const { data } = api.application.one.useQuery({ applicationId }); const { mutateAsync: deletePreviewDeployment, isLoading } = @@ -39,6 +31,21 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { enabled: !!applicationId, }, ); + + + const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { + deletePreviewDeployment({ + previewDeploymentId: previewDeploymentId, + }) + .then(() => { + refetchPreviewDeployments(); + toast.success("Preview deployment deleted"); + }) + .catch((error) => { + toast.error(error.message); + }); + }; + // const [url, setUrl] = React.useState(""); // useEffect(() => { // setUrl(document.location.origin); @@ -77,125 +84,43 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { {previewDeployments?.map((previewDeployment) => { const { deployments, domain } = previewDeployment; - return ( -
-
-
- {deployments?.length === 0 ? ( -
- - No deployments found - -
- ) : ( -
- - {previewDeployment?.pullRequestTitle} - - -
- )} -
- {previewDeployment?.pullRequestTitle && ( -
- - Title: {previewDeployment?.pullRequestTitle} - -
- )} - - {previewDeployment?.pullRequestURL && ( -
- - - Pull Request URL - -
- )} -
-
- Domain -
- - {domain?.host} - - - - -
-
-
- -
- {previewDeployment?.createdAt && ( -
- -
- )} - - - - - - - { - deletePreviewDeployment({ - previewDeploymentId: - previewDeployment.previewDeploymentId, - }) - .then(() => { - refetchPreviewDeployments(); - toast.success("Preview deployment deleted"); - }) - .catch((error) => { - toast.error(error.message); - }); - }} - > - - -
-
-
- ); - })} -
- )} - + return ( +
+
+ {deployments?.length === 0 ? ( +
+ + No deployments found + +
+ ) : ( + + )} +
+
+ ); + })} +
+ )} + ) : (
From 3a954746626eb65c270b4ffa2f5fc82f91578568 Mon Sep 17 00:00:00 2001 From: usopp Date: Thu, 19 Dec 2024 01:44:20 +0100 Subject: [PATCH 28/85] chore: lint --- .../preview-deployment-card.tsx | 14 +++++++------- .../show-preview-deployments.tsx | 7 ++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx index c5b381eb1..521f0afeb 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx @@ -13,20 +13,20 @@ import { DateTooltip } from "@/components/shared/date-tooltip"; interface PreviewDeploymentCardProps { appName: string; - serverId: string; + serverId: string | undefined; onDeploymentDelete: (deploymentId: string) => void; deploymentId: string; deploymentUrl: string; deployments: RouterOutputs["deployment"]["all"]; - domainId: string; - domainHost: string; + domainId: string | undefined; + domainHost: string | undefined; - pullRequestTitle: string; - pullRequestUrl: string; + pullRequestTitle: string | undefined; + pullRequestUrl: string | undefined; status: "running" | "error" | "done" | "idle" | undefined | null; - branch: string; - date: string; + branch: string | undefined; + date: string | undefined; isLoading: boolean; } diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 7be497ed7..2500662db 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,6 +1,3 @@ -import { DateTooltip } from "@/components/shared/date-tooltip"; -import { StatusTooltip } from "@/components/shared/status-tooltip"; -import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -10,7 +7,7 @@ import { } from "@/components/ui/card"; import { api } from "@/utils/api"; import { RocketIcon } from "lucide-react"; -import React, { useState } from "react"; +import React from "react"; import { toast } from "sonner"; import { PreviewDeploymentCard } from "./preview-deployment-card"; import { ShowPreviewSettings } from "./show-preview-settings"; @@ -95,7 +92,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
) : ( Date: Thu, 19 Dec 2024 01:58:18 +0100 Subject: [PATCH 29/85] chore: lint --- .../preview-deployment-card.tsx | 14 +++++++------- .../show-preview-deployments.tsx | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx index 521f0afeb..c5b381eb1 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx @@ -13,20 +13,20 @@ import { DateTooltip } from "@/components/shared/date-tooltip"; interface PreviewDeploymentCardProps { appName: string; - serverId: string | undefined; + serverId: string; onDeploymentDelete: (deploymentId: string) => void; deploymentId: string; deploymentUrl: string; deployments: RouterOutputs["deployment"]["all"]; - domainId: string | undefined; - domainHost: string | undefined; + domainId: string; + domainHost: string; - pullRequestTitle: string | undefined; - pullRequestUrl: string | undefined; + pullRequestTitle: string; + pullRequestUrl: string; status: "running" | "error" | "done" | "idle" | undefined | null; - branch: string | undefined; - date: string | undefined; + branch: string; + date: string; isLoading: boolean; } diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 2500662db..0c060afd6 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -99,15 +99,15 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { deploymentId={previewDeployment.previewDeploymentId} deploymentUrl={`http://${domain?.host}`} deployments={previewDeployment?.deployments || []} - domainId={domain?.domainId} - domainHost={domain?.host} + domainId={domain?.domainId || ""} + domainHost={domain?.host || ""} pullRequestTitle={ - previewDeployment?.pullRequestTitle + previewDeployment?.pullRequestTitle || "" } - pullRequestUrl={previewDeployment?.pullRequestURL} + pullRequestUrl={previewDeployment?.pullRequestURL || ""} status={previewDeployment.previewStatus} - branch={previewDeployment?.branch} - date={previewDeployment?.createdAt} + branch={previewDeployment?.branch || ""} + date={previewDeployment?.createdAt || ""} isLoading={isLoading} /> )} From 0abf62dd5212b2bc996c9d8ddfd4d3a7d8ccced7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 21 Dec 2024 13:10:34 -0600 Subject: [PATCH 30/85] refactor: prevent update appName in services --- packages/server/src/db/schema/utils.ts | 4 ++-- packages/server/src/services/application.ts | 4 ++-- packages/server/src/services/compose.ts | 4 ++-- packages/server/src/services/mariadb.ts | 4 ++-- packages/server/src/services/mongo.ts | 4 ++-- packages/server/src/services/mysql.ts | 4 ++-- packages/server/src/services/postgres.ts | 4 ++-- packages/server/src/services/redis.ts | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/server/src/db/schema/utils.ts b/packages/server/src/db/schema/utils.ts index e86517697..43332c8ad 100644 --- a/packages/server/src/db/schema/utils.ts +++ b/packages/server/src/db/schema/utils.ts @@ -17,9 +17,9 @@ export const generateAppName = (type: string) => { export const cleanAppName = (appName?: string) => { if (!appName) { - return appName; + return appName?.toLowerCase(); } - return appName.trim().replace(/ /g, "-"); + return appName.trim().replace(/ /g, "-").toLowerCase(); }; export const buildAppName = (type: string, baseAppName?: string) => { diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index d2ce34320..b8ecb88bc 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -138,11 +138,11 @@ export const updateApplication = async ( applicationId: string, applicationData: Partial, ) => { + const { appName, ...rest } = applicationData; const application = await db .update(applications) .set({ - ...applicationData, - appName: cleanAppName(applicationData.appName), + ...rest, }) .where(eq(applications.applicationId, applicationId)) .returning(); diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 27e7bfa47..5e5c68978 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -184,11 +184,11 @@ export const updateCompose = async ( composeId: string, composeData: Partial, ) => { + const { appName, ...rest } = composeData; const composeResult = await db .update(compose) .set({ - ...composeData, - appName: cleanAppName(composeData.appName), + ...rest, }) .where(eq(compose.composeId, composeId)) .returning(); diff --git a/packages/server/src/services/mariadb.ts b/packages/server/src/services/mariadb.ts index 7906cc5b7..39ef5910a 100644 --- a/packages/server/src/services/mariadb.ts +++ b/packages/server/src/services/mariadb.ts @@ -80,11 +80,11 @@ export const updateMariadbById = async ( mariadbId: string, mariadbData: Partial, ) => { + const { appName, ...rest } = mariadbData; const result = await db .update(mariadb) .set({ - ...mariadbData, - appName: cleanAppName(mariadbData.appName), + ...rest, }) .where(eq(mariadb.mariadbId, mariadbId)) .returning(); diff --git a/packages/server/src/services/mongo.ts b/packages/server/src/services/mongo.ts index 82a8a84f6..f8d5e4d64 100644 --- a/packages/server/src/services/mongo.ts +++ b/packages/server/src/services/mongo.ts @@ -72,11 +72,11 @@ export const updateMongoById = async ( mongoId: string, mongoData: Partial, ) => { + const { appName, ...rest } = mongoData; const result = await db .update(mongo) .set({ - ...mongoData, - appName: cleanAppName(mongoData.appName), + ...rest, }) .where(eq(mongo.mongoId, mongoId)) .returning(); diff --git a/packages/server/src/services/mysql.ts b/packages/server/src/services/mysql.ts index 1bb2c4787..e2c8bb6fb 100644 --- a/packages/server/src/services/mysql.ts +++ b/packages/server/src/services/mysql.ts @@ -76,11 +76,11 @@ export const updateMySqlById = async ( mysqlId: string, mysqlData: Partial, ) => { + const { appName, ...rest } = mysqlData; const result = await db .update(mysql) .set({ - ...mysqlData, - appName: cleanAppName(mysqlData.appName), + ...rest, }) .where(eq(mysql.mysqlId, mysqlId)) .returning(); diff --git a/packages/server/src/services/postgres.ts b/packages/server/src/services/postgres.ts index d2cd4874b..87286e67d 100644 --- a/packages/server/src/services/postgres.ts +++ b/packages/server/src/services/postgres.ts @@ -94,11 +94,11 @@ export const updatePostgresById = async ( postgresId: string, postgresData: Partial, ) => { + const { appName, ...rest } = postgresData; const result = await db .update(postgres) .set({ - ...postgresData, - appName: cleanAppName(postgresData.appName), + ...rest, }) .where(eq(postgres.postgresId, postgresId)) .returning(); diff --git a/packages/server/src/services/redis.ts b/packages/server/src/services/redis.ts index 96b3066c3..5b958081b 100644 --- a/packages/server/src/services/redis.ts +++ b/packages/server/src/services/redis.ts @@ -68,11 +68,11 @@ export const updateRedisById = async ( redisId: string, redisData: Partial, ) => { + const { appName, ...rest } = redisData; const result = await db .update(redis) .set({ - ...redisData, - appName: cleanAppName(redisData.appName), + ...rest, }) .where(eq(redis.redisId, redisId)) .returning(); From 29ce8908eeb913c3e20a2f5fd70f1d8c3cf5a3fe Mon Sep 17 00:00:00 2001 From: 190km Date: Sat, 21 Dec 2024 23:11:55 +0100 Subject: [PATCH 31/85] refactor: remove all the props except id, serverid, isloading, and delete function --- .../preview-deployment-card.tsx | 195 +++++++----------- .../show-preview-deployments.tsx | 91 +++----- 2 files changed, 105 insertions(+), 181 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx index c5b381eb1..4d61fdec9 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx @@ -1,134 +1,99 @@ -import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; -import { Clock, GitBranch, GitPullRequest, Pencil } from "lucide-react"; +import { Clock, GitBranch, GitPullRequest } from "lucide-react"; import Link from "next/link"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { DialogAction } from "@/components/shared/dialog-action"; +import { api } from "@/utils/api"; import { ShowPreviewBuilds } from "./show-preview-builds"; -import { RouterOutputs } from "@/utils/api"; -import { AddPreviewDomain } from "./add-preview-domain"; import { DateTooltip } from "@/components/shared/date-tooltip"; interface PreviewDeploymentCardProps { - appName: string; - serverId: string; - onDeploymentDelete: (deploymentId: string) => void; - deploymentId: string; - deploymentUrl: string; - deployments: RouterOutputs["deployment"]["all"]; - - domainId: string; - domainHost: string; - - pullRequestTitle: string; - pullRequestUrl: string; - status: "running" | "error" | "done" | "idle" | undefined | null; - branch: string; - date: string; - isLoading: boolean; + deploymentId: string; + serverId: string; + onDeploymentDelete: (deploymentId: string) => void; + isLoading: boolean; } export function PreviewDeploymentCard({ - appName, - serverId, - - onDeploymentDelete, - deploymentId, - deployments, - - domainId, - domainHost, - - pullRequestTitle, - pullRequestUrl, - isLoading, - status, - branch, - date, + deploymentId, + serverId, + onDeploymentDelete, + isLoading, }: PreviewDeploymentCardProps) { - return ( -
-
- {pullRequestTitle} - - - {status - ?.replace("running", "Running") - .replace("done", "Done") - .replace("error", "Error") - .replace("idle", "Idle") || "Idle"} - -
-
-
-
- - {domainHost} - -
- - - -
-
-
- - Branch: - - {" "} - {branch} - -
-
- - Deployed: - - - -
-
- -
-

Pull Request

-
- - - {pullRequestTitle} - -
-
-
-
-
- + const { data: previewDeployment } = api.previewDeployment.one.useQuery({ + previewDeploymentId: deploymentId, + }); - - - + if (!previewDeployment) return null; - onDeploymentDelete(deploymentId)} - > - + + onDeploymentDelete(deploymentId)} + > +
+ )} + ) : (
From 8c06296503050c67f57ce75df0018be0e2e19f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Sun, 22 Dec 2024 00:39:18 -0300 Subject: [PATCH 32/85] feat: add decoration column to discord notification --- apps/dokploy/drizzle/0052_bumpy_luckman.sql | 1 + apps/dokploy/drizzle/meta/0052_snapshot.json | 4246 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 7 + packages/server/src/db/schema/notification.ts | 1 + 4 files changed, 4255 insertions(+) create mode 100644 apps/dokploy/drizzle/0052_bumpy_luckman.sql create mode 100644 apps/dokploy/drizzle/meta/0052_snapshot.json diff --git a/apps/dokploy/drizzle/0052_bumpy_luckman.sql b/apps/dokploy/drizzle/0052_bumpy_luckman.sql new file mode 100644 index 000000000..972187359 --- /dev/null +++ b/apps/dokploy/drizzle/0052_bumpy_luckman.sql @@ -0,0 +1 @@ +ALTER TABLE "discord" ADD COLUMN "decoration" boolean; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0052_snapshot.json b/apps/dokploy/drizzle/meta/0052_snapshot.json new file mode 100644 index 000000000..6debca88b --- /dev/null +++ b/apps/dokploy/drizzle/meta/0052_snapshot.json @@ -0,0 +1,4246 @@ +{ + "id": "c5eb96f1-db46-4498-8bf4-417f89422de4", + "prevId": "0f21aab4-69a8-4ca9-91fa-7a819774e5ea", + "version": "6", + "dialect": "postgresql", + "tables": { + "public.application": { + "name": "application", + "schema": "", + "columns": { + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewEnv": { + "name": "previewEnv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewBuildArgs": { + "name": "previewBuildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewWildcard": { + "name": "previewWildcard", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewPort": { + "name": "previewPort", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "previewHttps": { + "name": "previewHttps", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "previewPath": { + "name": "previewPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "previewLimit": { + "name": "previewLimit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "isPreviewDeploymentsActive": { + "name": "isPreviewDeploymentsActive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "buildArgs": { + "name": "buildArgs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buildPath": { + "name": "buildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBuildPath": { + "name": "gitlabBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBuildPath": { + "name": "bitbucketBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBuildPath": { + "name": "customGitBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerfile": { + "name": "dockerfile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerContextPath": { + "name": "dockerContextPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerBuildStage": { + "name": "dockerBuildStage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dropBuildPath": { + "name": "dropBuildPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "healthCheckSwarm": { + "name": "healthCheckSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "restartPolicySwarm": { + "name": "restartPolicySwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "placementSwarm": { + "name": "placementSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "updateConfigSwarm": { + "name": "updateConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "rollbackConfigSwarm": { + "name": "rollbackConfigSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "modeSwarm": { + "name": "modeSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "labelsSwarm": { + "name": "labelsSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "networkSwarm": { + "name": "networkSwarm", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "replicas": { + "name": "replicas", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "buildType": { + "name": "buildType", + "type": "buildType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nixpacks'" + }, + "herokuVersion": { + "name": "herokuVersion", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'24'" + }, + "publishDirectory": { + "name": "publishDirectory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "application", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_registryId_registry_registryId_fk": { + "name": "application_registryId_registry_registryId_fk", + "tableFrom": "application", + "tableTo": "registry", + "columnsFrom": [ + "registryId" + ], + "columnsTo": [ + "registryId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_projectId_project_projectId_fk": { + "name": "application_projectId_project_projectId_fk", + "tableFrom": "application", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "application_githubId_github_githubId_fk": { + "name": "application_githubId_github_githubId_fk", + "tableFrom": "application", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_gitlabId_gitlab_gitlabId_fk": { + "name": "application_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "application", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "application_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "application", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "application_serverId_server_serverId_fk": { + "name": "application_serverId_server_serverId_fk", + "tableFrom": "application", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "application_appName_unique": { + "name": "application_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.postgres": { + "name": "postgres", + "schema": "", + "columns": { + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "postgres_projectId_project_projectId_fk": { + "name": "postgres_projectId_project_projectId_fk", + "tableFrom": "postgres", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "postgres_serverId_server_serverId_fk": { + "name": "postgres_serverId_server_serverId_fk", + "tableFrom": "postgres", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "postgres_appName_unique": { + "name": "postgres_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isRegistered": { + "name": "isRegistered", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expirationDate": { + "name": "expirationDate", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canCreateProjects": { + "name": "canCreateProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToSSHKeys": { + "name": "canAccessToSSHKeys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canCreateServices": { + "name": "canCreateServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteProjects": { + "name": "canDeleteProjects", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canDeleteServices": { + "name": "canDeleteServices", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToDocker": { + "name": "canAccessToDocker", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToAPI": { + "name": "canAccessToAPI", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToGitProviders": { + "name": "canAccessToGitProviders", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "canAccessToTraefikFiles": { + "name": "canAccessToTraefikFiles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "accesedProjects": { + "name": "accesedProjects", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "accesedServices": { + "name": "accesedServices", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_adminId_admin_adminId_fk": { + "name": "user_adminId_admin_adminId_fk", + "tableFrom": "user", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_authId_auth_id_fk": { + "name": "user_authId_auth_id_fk", + "tableFrom": "user", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.admin": { + "name": "admin", + "schema": "", + "columns": { + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "serverIp": { + "name": "serverIp", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "letsEncryptEmail": { + "name": "letsEncryptEmail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sshPrivateKey": { + "name": "sshPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enableLogRotation": { + "name": "enableLogRotation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "authId": { + "name": "authId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serversQuantity": { + "name": "serversQuantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "admin_authId_auth_id_fk": { + "name": "admin_authId_auth_id_fk", + "tableFrom": "admin", + "tableTo": "auth", + "columnsFrom": [ + "authId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.auth": { + "name": "auth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rol": { + "name": "rol", + "type": "Roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is2FAEnabled": { + "name": "is2FAEnabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resetPasswordToken": { + "name": "resetPasswordToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resetPasswordExpiresAt": { + "name": "resetPasswordExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationToken": { + "name": "confirmationToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmationExpiresAt": { + "name": "confirmationExpiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_email_unique": { + "name": "auth_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "project_adminId_admin_adminId_fk": { + "name": "project_adminId_admin_adminId_fk", + "tableFrom": "project", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "https": { + "name": "https", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3000 + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/'" + }, + "serviceName": { + "name": "serviceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domainType": { + "name": "domainType", + "type": "domainType", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'application'" + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "certificateType": { + "name": "certificateType", + "type": "certificateType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + } + }, + "indexes": {}, + "foreignKeys": { + "domain_composeId_compose_composeId_fk": { + "name": "domain_composeId_compose_composeId_fk", + "tableFrom": "domain", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_applicationId_application_applicationId_fk": { + "name": "domain_applicationId_application_applicationId_fk", + "tableFrom": "domain", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "domain", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mariadb": { + "name": "mariadb", + "schema": "", + "columns": { + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mariadb_projectId_project_projectId_fk": { + "name": "mariadb_projectId_project_projectId_fk", + "tableFrom": "mariadb", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mariadb_serverId_server_serverId_fk": { + "name": "mariadb_serverId_server_serverId_fk", + "tableFrom": "mariadb", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mariadb_appName_unique": { + "name": "mariadb_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mongo": { + "name": "mongo", + "schema": "", + "columns": { + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mongo_projectId_project_projectId_fk": { + "name": "mongo_projectId_project_projectId_fk", + "tableFrom": "mongo", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mongo_serverId_server_serverId_fk": { + "name": "mongo_serverId_server_serverId_fk", + "tableFrom": "mongo", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mongo_appName_unique": { + "name": "mongo_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.mysql": { + "name": "mysql", + "schema": "", + "columns": { + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "databaseName": { + "name": "databaseName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseUser": { + "name": "databaseUser", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databasePassword": { + "name": "databasePassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rootPassword": { + "name": "rootPassword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mysql_projectId_project_projectId_fk": { + "name": "mysql_projectId_project_projectId_fk", + "tableFrom": "mysql", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mysql_serverId_server_serverId_fk": { + "name": "mysql_serverId_server_serverId_fk", + "tableFrom": "mysql", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mysql_appName_unique": { + "name": "mysql_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.backup": { + "name": "backup", + "schema": "", + "columns": { + "backupId": { + "name": "backupId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "database": { + "name": "database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "databaseType": { + "name": "databaseType", + "type": "databaseType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "backup_destinationId_destination_destinationId_fk": { + "name": "backup_destinationId_destination_destinationId_fk", + "tableFrom": "backup", + "tableTo": "destination", + "columnsFrom": [ + "destinationId" + ], + "columnsTo": [ + "destinationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_postgresId_postgres_postgresId_fk": { + "name": "backup_postgresId_postgres_postgresId_fk", + "tableFrom": "backup", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mariadbId_mariadb_mariadbId_fk": { + "name": "backup_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "backup", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mysqlId_mysql_mysqlId_fk": { + "name": "backup_mysqlId_mysql_mysqlId_fk", + "tableFrom": "backup", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_mongoId_mongo_mongoId_fk": { + "name": "backup_mongoId_mongo_mongoId_fk", + "tableFrom": "backup", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.destination": { + "name": "destination", + "schema": "", + "columns": { + "destinationId": { + "name": "destinationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessKey": { + "name": "accessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secretAccessKey": { + "name": "secretAccessKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "destination_adminId_admin_adminId_fk": { + "name": "destination_adminId_admin_adminId_fk", + "tableFrom": "destination", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.deployment": { + "name": "deployment", + "schema": "", + "columns": { + "deploymentId": { + "name": "deploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "deploymentStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'running'" + }, + "logPath": { + "name": "logPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isPreviewDeployment": { + "name": "isPreviewDeployment", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "deployment_applicationId_application_applicationId_fk": { + "name": "deployment_applicationId_application_applicationId_fk", + "tableFrom": "deployment", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_composeId_compose_composeId_fk": { + "name": "deployment_composeId_compose_composeId_fk", + "tableFrom": "deployment", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_serverId_server_serverId_fk": { + "name": "deployment_serverId_server_serverId_fk", + "tableFrom": "deployment", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": { + "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk", + "tableFrom": "deployment", + "tableTo": "preview_deployments", + "columnsFrom": [ + "previewDeploymentId" + ], + "columnsTo": [ + "previewDeploymentId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.mount": { + "name": "mount", + "schema": "", + "columns": { + "mountId": { + "name": "mountId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "mountType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hostPath": { + "name": "hostPath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumeName": { + "name": "volumeName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filePath": { + "name": "filePath", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serviceType": { + "name": "serviceType", + "type": "serviceType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'application'" + }, + "mountPath": { + "name": "mountPath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postgresId": { + "name": "postgresId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mariadbId": { + "name": "mariadbId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mongoId": { + "name": "mongoId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mysqlId": { + "name": "mysqlId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mount_applicationId_application_applicationId_fk": { + "name": "mount_applicationId_application_applicationId_fk", + "tableFrom": "mount", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_postgresId_postgres_postgresId_fk": { + "name": "mount_postgresId_postgres_postgresId_fk", + "tableFrom": "mount", + "tableTo": "postgres", + "columnsFrom": [ + "postgresId" + ], + "columnsTo": [ + "postgresId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mariadbId_mariadb_mariadbId_fk": { + "name": "mount_mariadbId_mariadb_mariadbId_fk", + "tableFrom": "mount", + "tableTo": "mariadb", + "columnsFrom": [ + "mariadbId" + ], + "columnsTo": [ + "mariadbId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mongoId_mongo_mongoId_fk": { + "name": "mount_mongoId_mongo_mongoId_fk", + "tableFrom": "mount", + "tableTo": "mongo", + "columnsFrom": [ + "mongoId" + ], + "columnsTo": [ + "mongoId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_mysqlId_mysql_mysqlId_fk": { + "name": "mount_mysqlId_mysql_mysqlId_fk", + "tableFrom": "mount", + "tableTo": "mysql", + "columnsFrom": [ + "mysqlId" + ], + "columnsTo": [ + "mysqlId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_redisId_redis_redisId_fk": { + "name": "mount_redisId_redis_redisId_fk", + "tableFrom": "mount", + "tableTo": "redis", + "columnsFrom": [ + "redisId" + ], + "columnsTo": [ + "redisId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mount_composeId_compose_composeId_fk": { + "name": "mount_composeId_compose_composeId_fk", + "tableFrom": "mount", + "tableTo": "compose", + "columnsFrom": [ + "composeId" + ], + "columnsTo": [ + "composeId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.certificate": { + "name": "certificate", + "schema": "", + "columns": { + "certificateId": { + "name": "certificateId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificateData": { + "name": "certificateData", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "certificatePath": { + "name": "certificatePath", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autoRenew": { + "name": "autoRenew", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificate_adminId_admin_adminId_fk": { + "name": "certificate_adminId_admin_adminId_fk", + "tableFrom": "certificate", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "certificate_serverId_server_serverId_fk": { + "name": "certificate_serverId_server_serverId_fk", + "tableFrom": "certificate", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "certificate_certificatePath_unique": { + "name": "certificate_certificatePath_unique", + "nullsNotDistinct": false, + "columns": [ + "certificatePath" + ] + } + } + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_auth_id_fk": { + "name": "session_user_id_auth_id_fk", + "tableFrom": "session", + "tableTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.redirect": { + "name": "redirect", + "schema": "", + "columns": { + "redirectId": { + "name": "redirectId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replacement": { + "name": "replacement", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permanent": { + "name": "permanent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "uniqueConfigKey": { + "name": "uniqueConfigKey", + "type": "serial", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_applicationId_application_applicationId_fk": { + "name": "redirect_applicationId_application_applicationId_fk", + "tableFrom": "redirect", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.security": { + "name": "security", + "schema": "", + "columns": { + "securityId": { + "name": "securityId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "security_applicationId_application_applicationId_fk": { + "name": "security_applicationId_application_applicationId_fk", + "tableFrom": "security", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "security_username_applicationId_unique": { + "name": "security_username_applicationId_unique", + "nullsNotDistinct": false, + "columns": [ + "username", + "applicationId" + ] + } + } + }, + "public.port": { + "name": "port", + "schema": "", + "columns": { + "portId": { + "name": "portId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "publishedPort": { + "name": "publishedPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "targetPort": { + "name": "targetPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "protocol": { + "name": "protocol", + "type": "protocolType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "port_applicationId_application_applicationId_fk": { + "name": "port_applicationId_application_applicationId_fk", + "tableFrom": "port", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.redis": { + "name": "redis", + "schema": "", + "columns": { + "redisId": { + "name": "redisId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dockerImage": { + "name": "dockerImage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memoryReservation": { + "name": "memoryReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "memoryLimit": { + "name": "memoryLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuReservation": { + "name": "cpuReservation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpuLimit": { + "name": "cpuLimit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "externalPort": { + "name": "externalPort", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationStatus": { + "name": "applicationStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "redis_projectId_project_projectId_fk": { + "name": "redis_projectId_project_projectId_fk", + "tableFrom": "redis", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "redis_serverId_server_serverId_fk": { + "name": "redis_serverId_server_serverId_fk", + "tableFrom": "redis", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "redis_appName_unique": { + "name": "redis_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + }, + "public.compose": { + "name": "compose", + "schema": "", + "columns": { + "composeId": { + "name": "composeId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "composeFile": { + "name": "composeFile", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sourceType": { + "name": "sourceType", + "type": "sourceTypeCompose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "composeType": { + "name": "composeType", + "type": "composeType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'docker-compose'" + }, + "repository": { + "name": "repository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "autoDeploy": { + "name": "autoDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gitlabProjectId": { + "name": "gitlabProjectId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitlabRepository": { + "name": "gitlabRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabOwner": { + "name": "gitlabOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabBranch": { + "name": "gitlabBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabPathNamespace": { + "name": "gitlabPathNamespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketRepository": { + "name": "bitbucketRepository", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketOwner": { + "name": "bitbucketOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketBranch": { + "name": "bitbucketBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitUrl": { + "name": "customGitUrl", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitBranch": { + "name": "customGitBranch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customGitSSHKeyId": { + "name": "customGitSSHKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "composePath": { + "name": "composePath", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'./docker-compose.yml'" + }, + "suffix": { + "name": "suffix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "randomize": { + "name": "randomize", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "composeStatus": { + "name": "composeStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "projectId": { + "name": "projectId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": { + "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "compose", + "tableTo": "ssh-key", + "columnsFrom": [ + "customGitSSHKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_projectId_project_projectId_fk": { + "name": "compose_projectId_project_projectId_fk", + "tableFrom": "compose", + "tableTo": "project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "projectId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "compose_githubId_github_githubId_fk": { + "name": "compose_githubId_github_githubId_fk", + "tableFrom": "compose", + "tableTo": "github", + "columnsFrom": [ + "githubId" + ], + "columnsTo": [ + "githubId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_gitlabId_gitlab_gitlabId_fk": { + "name": "compose_gitlabId_gitlab_gitlabId_fk", + "tableFrom": "compose", + "tableTo": "gitlab", + "columnsFrom": [ + "gitlabId" + ], + "columnsTo": [ + "gitlabId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_bitbucketId_bitbucket_bitbucketId_fk": { + "name": "compose_bitbucketId_bitbucket_bitbucketId_fk", + "tableFrom": "compose", + "tableTo": "bitbucket", + "columnsFrom": [ + "bitbucketId" + ], + "columnsTo": [ + "bitbucketId" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "compose_serverId_server_serverId_fk": { + "name": "compose_serverId_server_serverId_fk", + "tableFrom": "compose", + "tableTo": "server", + "columnsFrom": [ + "serverId" + ], + "columnsTo": [ + "serverId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.registry": { + "name": "registry", + "schema": "", + "columns": { + "registryId": { + "name": "registryId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registryName": { + "name": "registryName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "imagePrefix": { + "name": "imagePrefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "registryUrl": { + "name": "registryUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selfHosted": { + "name": "selfHosted", + "type": "RegistryType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'cloud'" + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "registry_adminId_admin_adminId_fk": { + "name": "registry_adminId_admin_adminId_fk", + "tableFrom": "registry", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.discord": { + "name": "discord", + "schema": "", + "columns": { + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decoration": { + "name": "decoration", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "smtpServer": { + "name": "smtpServer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "smtpPort": { + "name": "smtpPort", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fromAddress": { + "name": "fromAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "toAddress": { + "name": "toAddress", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "notificationId": { + "name": "notificationId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "appDeploy": { + "name": "appDeploy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "appBuildError": { + "name": "appBuildError", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "databaseBackup": { + "name": "databaseBackup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dokployRestart": { + "name": "dokployRestart", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "dockerCleanup": { + "name": "dockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notificationType": { + "name": "notificationType", + "type": "notificationType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discordId": { + "name": "discordId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailId": { + "name": "emailId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notification_slackId_slack_slackId_fk": { + "name": "notification_slackId_slack_slackId_fk", + "tableFrom": "notification", + "tableTo": "slack", + "columnsFrom": [ + "slackId" + ], + "columnsTo": [ + "slackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_telegramId_telegram_telegramId_fk": { + "name": "notification_telegramId_telegram_telegramId_fk", + "tableFrom": "notification", + "tableTo": "telegram", + "columnsFrom": [ + "telegramId" + ], + "columnsTo": [ + "telegramId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_discordId_discord_discordId_fk": { + "name": "notification_discordId_discord_discordId_fk", + "tableFrom": "notification", + "tableTo": "discord", + "columnsFrom": [ + "discordId" + ], + "columnsTo": [ + "discordId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_emailId_email_emailId_fk": { + "name": "notification_emailId_email_emailId_fk", + "tableFrom": "notification", + "tableTo": "email", + "columnsFrom": [ + "emailId" + ], + "columnsTo": [ + "emailId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_adminId_admin_adminId_fk": { + "name": "notification_adminId_admin_adminId_fk", + "tableFrom": "notification", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.slack": { + "name": "slack", + "schema": "", + "columns": { + "slackId": { + "name": "slackId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "webhookUrl": { + "name": "webhookUrl", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.telegram": { + "name": "telegram", + "schema": "", + "columns": { + "telegramId": { + "name": "telegramId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "botToken": { + "name": "botToken", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chatId": { + "name": "chatId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.ssh-key": { + "name": "ssh-key", + "schema": "", + "columns": { + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "privateKey": { + "name": "privateKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "publicKey": { + "name": "publicKey", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastUsedAt": { + "name": "lastUsedAt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ssh-key_adminId_admin_adminId_fk": { + "name": "ssh-key_adminId_admin_adminId_fk", + "tableFrom": "ssh-key", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.git_provider": { + "name": "git_provider", + "schema": "", + "columns": { + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerType": { + "name": "providerType", + "type": "gitProviderType", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "git_provider_adminId_admin_adminId_fk": { + "name": "git_provider_adminId_admin_adminId_fk", + "tableFrom": "git_provider", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.bitbucket": { + "name": "bitbucket", + "schema": "", + "columns": { + "bitbucketId": { + "name": "bitbucketId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "bitbucketUsername": { + "name": "bitbucketUsername", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "appPassword": { + "name": "appPassword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bitbucketWorkspaceName": { + "name": "bitbucketWorkspaceName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "bitbucket_gitProviderId_git_provider_gitProviderId_fk": { + "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "bitbucket", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.github": { + "name": "github", + "schema": "", + "columns": { + "githubId": { + "name": "githubId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "githubAppName": { + "name": "githubAppName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubAppId": { + "name": "githubAppId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "githubClientId": { + "name": "githubClientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubClientSecret": { + "name": "githubClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubInstallationId": { + "name": "githubInstallationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubPrivateKey": { + "name": "githubPrivateKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "githubWebhookSecret": { + "name": "githubWebhookSecret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "github_gitProviderId_git_provider_gitProviderId_fk": { + "name": "github_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "github", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.gitlab": { + "name": "gitlab", + "schema": "", + "columns": { + "gitlabId": { + "name": "gitlabId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "gitlabUrl": { + "name": "gitlabUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'https://gitlab.com'" + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gitProviderId": { + "name": "gitProviderId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "gitlab_gitProviderId_git_provider_gitProviderId_fk": { + "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk", + "tableFrom": "gitlab", + "tableTo": "git_provider", + "columnsFrom": [ + "gitProviderId" + ], + "columnsTo": [ + "gitProviderId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.server": { + "name": "server", + "schema": "", + "columns": { + "serverId": { + "name": "serverId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'root'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enableDockerCleanup": { + "name": "enableDockerCleanup", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "adminId": { + "name": "adminId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serverStatus": { + "name": "serverStatus", + "type": "serverStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sshKeyId": { + "name": "sshKeyId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "server_adminId_admin_adminId_fk": { + "name": "server_adminId_admin_adminId_fk", + "tableFrom": "server", + "tableTo": "admin", + "columnsFrom": [ + "adminId" + ], + "columnsTo": [ + "adminId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "server_sshKeyId_ssh-key_sshKeyId_fk": { + "name": "server_sshKeyId_ssh-key_sshKeyId_fk", + "tableFrom": "server", + "tableTo": "ssh-key", + "columnsFrom": [ + "sshKeyId" + ], + "columnsTo": [ + "sshKeyId" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.preview_deployments": { + "name": "preview_deployments", + "schema": "", + "columns": { + "previewDeploymentId": { + "name": "previewDeploymentId", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestId": { + "name": "pullRequestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestNumber": { + "name": "pullRequestNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestURL": { + "name": "pullRequestURL", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestTitle": { + "name": "pullRequestTitle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pullRequestCommentId": { + "name": "pullRequestCommentId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previewStatus": { + "name": "previewStatus", + "type": "applicationStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "appName": { + "name": "appName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domainId": { + "name": "domainId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "preview_deployments_applicationId_application_applicationId_fk": { + "name": "preview_deployments_applicationId_application_applicationId_fk", + "tableFrom": "preview_deployments", + "tableTo": "application", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "applicationId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_deployments_domainId_domain_domainId_fk": { + "name": "preview_deployments_domainId_domain_domainId_fk", + "tableFrom": "preview_deployments", + "tableTo": "domain", + "columnsFrom": [ + "domainId" + ], + "columnsTo": [ + "domainId" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "preview_deployments_appName_unique": { + "name": "preview_deployments_appName_unique", + "nullsNotDistinct": false, + "columns": [ + "appName" + ] + } + } + } + }, + "enums": { + "public.buildType": { + "name": "buildType", + "schema": "public", + "values": [ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static" + ] + }, + "public.sourceType": { + "name": "sourceType", + "schema": "public", + "values": [ + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "drop" + ] + }, + "public.Roles": { + "name": "Roles", + "schema": "public", + "values": [ + "admin", + "user" + ] + }, + "public.domainType": { + "name": "domainType", + "schema": "public", + "values": [ + "compose", + "application", + "preview" + ] + }, + "public.databaseType": { + "name": "databaseType", + "schema": "public", + "values": [ + "postgres", + "mariadb", + "mysql", + "mongo" + ] + }, + "public.deploymentStatus": { + "name": "deploymentStatus", + "schema": "public", + "values": [ + "running", + "done", + "error" + ] + }, + "public.mountType": { + "name": "mountType", + "schema": "public", + "values": [ + "bind", + "volume", + "file" + ] + }, + "public.serviceType": { + "name": "serviceType", + "schema": "public", + "values": [ + "application", + "postgres", + "mysql", + "mariadb", + "mongo", + "redis", + "compose" + ] + }, + "public.protocolType": { + "name": "protocolType", + "schema": "public", + "values": [ + "tcp", + "udp" + ] + }, + "public.applicationStatus": { + "name": "applicationStatus", + "schema": "public", + "values": [ + "idle", + "running", + "done", + "error" + ] + }, + "public.certificateType": { + "name": "certificateType", + "schema": "public", + "values": [ + "letsencrypt", + "none" + ] + }, + "public.composeType": { + "name": "composeType", + "schema": "public", + "values": [ + "docker-compose", + "stack" + ] + }, + "public.sourceTypeCompose": { + "name": "sourceTypeCompose", + "schema": "public", + "values": [ + "git", + "github", + "gitlab", + "bitbucket", + "raw" + ] + }, + "public.RegistryType": { + "name": "RegistryType", + "schema": "public", + "values": [ + "selfHosted", + "cloud" + ] + }, + "public.notificationType": { + "name": "notificationType", + "schema": "public", + "values": [ + "slack", + "telegram", + "discord", + "email" + ] + }, + "public.gitProviderType": { + "name": "gitProviderType", + "schema": "public", + "values": [ + "github", + "gitlab", + "bitbucket" + ] + }, + "public.serverStatus": { + "name": "serverStatus", + "schema": "public", + "values": [ + "active", + "inactive" + ] + } + }, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index eeb102573..e0d4321d1 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -365,6 +365,13 @@ "when": 1734241482851, "tag": "0051_hard_gorgon", "breakpoints": true + }, + { + "idx": 52, + "version": "6", + "when": 1734809337308, + "tag": "0052_bumpy_luckman", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index 73ee64cd7..ecd25ff21 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -68,6 +68,7 @@ export const discord = pgTable("discord", { .primaryKey() .$defaultFn(() => nanoid()), webhookUrl: text("webhookUrl").notNull(), + decoration: boolean("decoration"), }); export const email = pgTable("email", { From 055b59e6fa3c858d6a8bbc1fe5df6792e88fa3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Sun, 22 Dec 2024 00:42:00 -0300 Subject: [PATCH 33/85] feat: discord decoration switch --- .../notifications/add-notification.tsx | 58 +++++++--- .../notifications/delete-notification.tsx | 8 +- .../notifications/show-notifications.tsx | 102 +++++++++--------- .../notifications/update-notification.tsx | 63 +++++++---- packages/server/src/db/schema/notification.ts | 2 + packages/server/src/services/notification.ts | 2 + 6 files changed, 145 insertions(+), 90 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx index 74a933a69..a8a8470d0 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx @@ -64,6 +64,7 @@ export const notificationSchema = z.discriminatedUnion("type", [ .object({ type: z.literal("discord"), webhookUrl: z.string().min(1, { message: "Webhook URL is required" }), + decoration: z.boolean().default(true), }) .merge(notificationBaseSchema), z @@ -195,6 +196,7 @@ export const AddNotification = () => { dokployRestart: dokployRestart, databaseBackup: databaseBackup, webhookUrl: data.webhookUrl, + decoration: data.decoration, name: data.name, dockerCleanup: dockerCleanup, }); @@ -397,23 +399,47 @@ export const AddNotification = () => { )} {type === "discord" && ( - ( - - Webhook URL - - - + <> + ( + + Webhook URL + + + - - - )} - /> + + + )} + /> + + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + )} {type === "email" && ( diff --git a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx index 4bb197b29..02c95d8b3 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx @@ -24,12 +24,12 @@ export const DeleteNotification = ({ notificationId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 10ea7304e..1883d5386 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -40,58 +40,60 @@ export const ShowNotifications = () => {
) : (
-
- {data?.map((notification, index) => ( -
-
- {notification.notificationType === "slack" && ( -
- -
- )} - {notification.notificationType === "telegram" && ( -
- -
- )} - {notification.notificationType === "discord" && ( -
- -
- )} - {notification.notificationType === "email" && ( -
- -
- )} -
- - {notification.name} - - - {notification.notificationType?.[0]?.toUpperCase() + notification.notificationType?.slice(1)} notification - -
-
-
- - -
+
+ {data?.map((notification, index) => ( +
+
+ {notification.notificationType === "slack" && ( +
+ +
+ )} + {notification.notificationType === "telegram" && ( +
+ +
+ )} + {notification.notificationType === "discord" && ( +
+ +
+ )} + {notification.notificationType === "email" && ( +
+ +
+ )} +
+ + {notification.name} + + + {notification.notificationType?.[0]?.toUpperCase() + + notification.notificationType?.slice(1)}{" "} + notification + +
+
+
+ + +
+
+ ))} +
+ +
+
- ))} -
- -
-
-
)} diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx index cfa2e0bab..533096fdd 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx @@ -28,7 +28,7 @@ import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { Mail, Pen } from "lucide-react"; import { useEffect, useState } from "react"; -import { FieldErrors, useFieldArray, useForm } from "react-hook-form"; +import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { type NotificationSchema, @@ -113,6 +113,7 @@ export const UpdateNotification = ({ notificationId }: Props) => { databaseBackup: data.databaseBackup, type: data.notificationType, webhookUrl: data.discord?.webhookUrl, + decoration: data.discord?.decoration || undefined, name: data.name, dockerCleanup: data.dockerCleanup, }); @@ -218,9 +219,7 @@ export const UpdateNotification = ({ notificationId }: Props) => { return ( - @@ -358,23 +357,47 @@ export const UpdateNotification = ({ notificationId }: Props) => { )} {type === "discord" && ( - ( - - Webhook URL - - - + <> + ( + + Webhook URL + + + - - - )} - /> + + + )} + /> + + ( + +
+ Decoration + + Decorate the notification with emojis. + +
+ + + +
+ )} + /> + )} {type === "email" && ( <> diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index ecd25ff21..80de897b3 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -172,6 +172,7 @@ export const apiCreateDiscord = notificationsSchema }) .extend({ webhookUrl: z.string().min(1), + decoration: z.boolean(), }) .required(); @@ -183,6 +184,7 @@ export const apiUpdateDiscord = apiCreateDiscord.partial().extend({ export const apiTestDiscordConnection = apiCreateDiscord.pick({ webhookUrl: true, + decoration: true, }); export const apiCreateEmail = notificationsSchema diff --git a/packages/server/src/services/notification.ts b/packages/server/src/services/notification.ts index 22e1e0b5e..e75154dfc 100644 --- a/packages/server/src/services/notification.ts +++ b/packages/server/src/services/notification.ts @@ -204,6 +204,7 @@ export const createDiscordNotification = async ( .insert(discord) .values({ webhookUrl: input.webhookUrl, + decoration: input.decoration, }) .returning() .then((value) => value[0]); @@ -272,6 +273,7 @@ export const updateDiscordNotification = async ( .update(discord) .set({ webhookUrl: input.webhookUrl, + decoration: input.decoration, }) .where(eq(discord.discordId, input.discordId)) .returning() From 870e074825e9e919f9d88d43997c4f16d2d72758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Sun, 22 Dec 2024 00:44:25 -0300 Subject: [PATCH 34/85] feat: implement decoration to embeds --- apps/dokploy/server/api/routers/auth.ts | 4 +-- .../server/api/routers/notification.ts | 8 ++++-- .../src/utils/notifications/build-error.ts | 27 ++++++++++--------- .../src/utils/notifications/build-success.ts | 21 ++++++++------- .../utils/notifications/database-backup.ts | 25 +++++++++-------- .../src/utils/notifications/docker-cleanup.ts | 13 +++++---- .../utils/notifications/dokploy-restart.ts | 11 +++++--- 7 files changed, 64 insertions(+), 45 deletions(-) diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index a1345cca3..b02ab617c 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -363,7 +363,7 @@ export const authRouter = createTRPCRouter({ Reset Password - + `, ); }), @@ -505,7 +505,7 @@ export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => { webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", }, { - title: " New User Registered", + title: "New User Registered", color: 0x00ff00, fields: [ { diff --git a/apps/dokploy/server/api/routers/notification.ts b/apps/dokploy/server/api/routers/notification.ts index 951d2a10d..ba226d457 100644 --- a/apps/dokploy/server/api/routers/notification.ts +++ b/apps/dokploy/server/api/routers/notification.ts @@ -187,11 +187,15 @@ export const notificationRouter = createTRPCRouter({ .input(apiTestDiscordConnection) .mutation(async ({ input }) => { try { + const decorate = (decoration: string, text: string) => + `${input.decoration ? decoration : ""} ${text}`.trim(); + await sendDiscordNotification(input, { - title: "> `🤚` - Test Notification", - description: "> Hi, From Dokploy 👋", + title: decorate(">", "`🤚` - Test Notification"), + description: decorate(">", "Hi, From Dokploy 👋"), color: 0xf3f7f4, }); + return true; } catch (error) { throw new TRPCError({ diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index f9286fc7f..695b37863 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -59,46 +59,49 @@ export const sendBuildErrorNotifications = async ({ } if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendDiscordNotification(discord, { - title: "> `⚠️` Build Failed", + title: decorate(">", "`⚠️` Build Failed"), color: 0xed4245, fields: [ { - name: "`🛠️` Project", + name: decorate("`🛠️`", "Project"), value: projectName, inline: true, }, { - name: "`⚙️` Application", + name: decorate("`⚙️`", "Application"), value: applicationName, inline: true, }, { - name: "`❔` Type", + name: decorate("`❔`", "Type"), value: applicationType, inline: true, }, { - name: "`📅` Date", + name: decorate("`📅`", "Date"), value: ``, inline: true, }, { - name: "`⌚` Time", + name: decorate("`⌚`", "Time"), value: ``, inline: true, }, { - name: "`❓`Type", + name: decorate("`❓`", "Type"), value: "Failed", inline: true, }, { - name: "`⚠️` Error Message", + name: decorate("`⚠️`", "Error Message"), value: `\`\`\`${errorMessage}\`\`\``, }, { - name: "`🧷` Build Link", + name: decorate("`🧷`", "Build Link"), value: `[Click here to access build link](${buildLink})`, }, ], @@ -114,15 +117,15 @@ export const sendBuildErrorNotifications = async ({ telegram, ` ⚠️ Build Failed - + Project: ${projectName} Application: ${applicationName} Type: ${applicationType} Time: ${date.toLocaleString()} - + Error:
${errorMessage}
- + Build Details: ${buildLink} `, ); diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 97fe7e1cf..402b0cd2c 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -57,42 +57,45 @@ export const sendBuildSuccessNotifications = async ({ } if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendDiscordNotification(discord, { title: "> `✅` Build Success", color: 0x57f287, fields: [ { - name: "`🛠️` Project", + name: decorate("`🛠️`", "Project"), value: projectName, inline: true, }, { - name: "`⚙️` Application", + name: decorate("`⚙️`", "Application"), value: applicationName, inline: true, }, { - name: "`❔` Application Type", + name: decorate("`❔`", "Type"), value: applicationType, inline: true, }, { - name: "`📅` Date", + name: decorate("`📅`", "Date"), value: ``, inline: true, }, { - name: "`⌚` Time", + name: decorate("`⌚`", "Time"), value: ``, inline: true, }, { - name: "`❓` Type", + name: decorate("`❓`", "Type"), value: "Successful", inline: true, }, { - name: "`🧷` Build Link", + name: decorate("`🧷`", "Build Link"), value: `[Click here to access build link](${buildLink})`, }, ], @@ -108,12 +111,12 @@ export const sendBuildSuccessNotifications = async ({ telegram, ` ✅ Build Success - + Project: ${projectName} Application: ${applicationName} Type: ${applicationType} Time: ${date.toLocaleString()} - + Build Details: ${buildLink} `, ); diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index d2dccdafa..3aec6f3d3 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -62,40 +62,43 @@ export const sendDatabaseBackupNotifications = async ({ } if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendDiscordNotification(discord, { title: type === "success" - ? "> `✅` Database Backup Successful" - : "> `❌` Database Backup Failed", + ? decorate(">", "`✅` Database Backup Successful") + : decorate(">", "`❌` Database Backup Failed"), color: type === "success" ? 0x57f287 : 0xed4245, fields: [ { - name: "`🛠️` Project", + name: decorate("`🛠️`", "Project"), value: projectName, inline: true, }, { - name: "`⚙️` Application", + name: decorate("`⚙️`", "Application"), value: applicationName, inline: true, }, { - name: "`❔` Database", + name: decorate("`❔`", "Database"), value: databaseType, inline: true, }, { - name: "`📅` Date", + name: decorate("`📅`", "Date"), value: ``, inline: true, }, { - name: "`⌚` Time", + name: decorate("`⌚`", "Time"), value: ``, inline: true, }, { - name: "`❓` Type", + name: decorate("`❓`", "Type"), value: type .replace("error", "Failed") .replace("success", "Successful"), @@ -104,7 +107,7 @@ export const sendDatabaseBackupNotifications = async ({ ...(type === "error" && errorMessage ? [ { - name: "`⚠️` Error Message", + name: decorate("`⚠️`", "Error Message"), value: `\`\`\`${errorMessage}\`\`\``, }, ] @@ -121,12 +124,12 @@ export const sendDatabaseBackupNotifications = async ({ const statusEmoji = type === "success" ? "✅" : "❌"; const messageText = ` ${statusEmoji} Database Backup ${type === "success" ? "Successful" : "Failed"} - + Project: ${projectName} Application: ${applicationName} Type: ${databaseType} Time: ${date.toLocaleString()} - + Status: ${type === "success" ? "Successful" : "Failed"} ${type === "error" && errorMessage ? `Error: ${errorMessage}` : ""} `; diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index 515d1ddc4..c95c79067 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -45,27 +45,30 @@ export const sendDockerCleanupNotifications = async ( } if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendDiscordNotification(discord, { - title: "> `✅` Docker Cleanup", + title: decorate(">", "`✅` Docker Cleanup"), color: 0x57f287, fields: [ { - name: "`📅` Date", + name: decorate("`📅`", "Date"), value: ``, inline: true, }, { - name: "`⌚` Time", + name: decorate("`⌚`", "Time"), value: ``, inline: true, }, { - name: "`❓` Type", + name: decorate("`❓`", "Type"), value: "Successful", inline: true, }, { - name: "`📜` Message", + name: decorate("`📜`", "Message"), value: `\`\`\`${message}\`\`\``, }, ], diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts index 883a3d1f1..16170349b 100644 --- a/packages/server/src/utils/notifications/dokploy-restart.ts +++ b/packages/server/src/utils/notifications/dokploy-restart.ts @@ -34,22 +34,25 @@ export const sendDokployRestartNotifications = async () => { } if (discord) { + const decorate = (decoration: string, text: string) => + `${discord.decoration ? decoration : ""} ${text}`.trim(); + await sendDiscordNotification(discord, { - title: "> `✅` Dokploy Server Restarted", + title: decorate(">", "`✅` Dokploy Server Restarted"), color: 0x57f287, fields: [ { - name: "`📅` Date", + name: decorate("`📅`", "Date"), value: ``, inline: true, }, { - name: "`⌚` Time", + name: decorate("`⌚`", "Time"), value: ``, inline: true, }, { - name: "`❓` Type", + name: decorate("`❓`", "Type"), value: "Successful", inline: true, }, From 21b2cce7a0cdafb38f245465bf103fb8898f6e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Sun, 22 Dec 2024 01:06:47 -0300 Subject: [PATCH 35/85] fix: send decoration prop to notification tester --- .../dashboard/settings/notifications/add-notification.tsx | 1 + .../dashboard/settings/notifications/update-notification.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx index a8a8470d0..639ea6ea2 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/add-notification.tsx @@ -734,6 +734,7 @@ export const AddNotification = () => { } else if (type === "discord") { await testDiscordConnection({ webhookUrl: form.getValues("webhookUrl"), + decoration: form.getValues("decoration"), }); } else if (type === "email") { await testEmailConnection({ diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx index 533096fdd..f9c75cd9b 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx @@ -692,6 +692,7 @@ export const UpdateNotification = ({ notificationId }: Props) => { } else if (type === "discord") { await testDiscordConnection({ webhookUrl: form.getValues("webhookUrl"), + decoration: form.getValues("decoration"), }); } else if (type === "email") { await testEmailConnection({ From 6e262cde0dabb3747719c58c0fcfd3f6057ef20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Sun, 22 Dec 2024 01:07:29 -0300 Subject: [PATCH 36/85] fix: update discord notification decoration --- .../dashboard/settings/notifications/update-notification.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx index f9c75cd9b..457919175 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx @@ -179,6 +179,7 @@ export const UpdateNotification = ({ notificationId }: Props) => { dokployRestart: dokployRestart, databaseBackup: databaseBackup, webhookUrl: formData.webhookUrl, + decoration: formData.decoration, name: formData.name, notificationId: notificationId, discordId: data?.discordId, @@ -379,7 +380,6 @@ export const UpdateNotification = ({ notificationId }: Props) => { (
From 80bd80b786076aad632d65b0f4125fedb548ee91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Sun, 22 Dec 2024 01:08:34 -0300 Subject: [PATCH 37/85] refactor: make decoration prop optional at discord tester schema --- packages/server/src/db/schema/notification.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/server/src/db/schema/notification.ts b/packages/server/src/db/schema/notification.ts index 80de897b3..5501621dc 100644 --- a/packages/server/src/db/schema/notification.ts +++ b/packages/server/src/db/schema/notification.ts @@ -182,10 +182,13 @@ export const apiUpdateDiscord = apiCreateDiscord.partial().extend({ adminId: z.string().optional(), }); -export const apiTestDiscordConnection = apiCreateDiscord.pick({ - webhookUrl: true, - decoration: true, -}); +export const apiTestDiscordConnection = apiCreateDiscord + .pick({ + webhookUrl: true, + }) + .extend({ + decoration: z.boolean().optional(), + }); export const apiCreateEmail = notificationsSchema .pick({ From a710728e77a7d5e83965a24055b923f0b7bb059a Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 22 Dec 2024 01:51:05 -0600 Subject: [PATCH 38/85] feat: add support for custom command --- .../dashboard/compose/advanced/add-command.tsx | 9 ++++++++- packages/server/src/utils/builders/compose.ts | 12 ++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx index 44ce15c04..6d1b455f9 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Card, @@ -91,7 +92,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
Run Command - Append a custom command to the compose file + Override a custom command to the compose file
@@ -101,6 +102,12 @@ export const AddCommandCompose = ({ composeId }: Props) => { onSubmit={form.handleSubmit(onSubmit)} className="grid w-full gap-4" > + + Modifying the default command may affect deployment stability, + impacting logs and monitoring. Proceed carefully and test + thoroughly. By default, the command starts with{" "} + docker. +
{ export const createCommand = (compose: ComposeNested) => { const { composeType, appName, sourceType } = compose; + if (compose.command) { + return `${sanitizeCommand(compose.command)}`; + } + const path = sourceType === "raw" ? "docker-compose.yml" : compose.composePath; let command = ""; @@ -154,12 +160,6 @@ export const createCommand = (compose: ComposeNested) => { command = `stack deploy -c ${path} ${appName} --prune`; } - const customCommand = sanitizeCommand(compose.command); - - if (customCommand) { - command = `${command} ${customCommand}`; - } - return command; }; From c0e9670daf1fb2799e3afa524d597204e3fcd620 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 22 Dec 2024 01:53:30 -0600 Subject: [PATCH 39/85] refactor: delete log --- packages/server/src/utils/builders/compose.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index 583c739b3..40f8d6d5b 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -49,7 +49,6 @@ Compose Type: ${composeType} ✅`; const projectPath = join(COMPOSE_PATH, compose.appName, "code"); - console.log("Command:", ...command.split(" ")); await spawnAsync( "docker", [...command.split(" ")], From 5c5066bc721982725c59aa6b6ac8c9d6a2859540 Mon Sep 17 00:00:00 2001 From: DJKnaeckebrot Date: Sun, 22 Dec 2024 20:07:48 +0100 Subject: [PATCH 40/85] chore: remove server from swarms page --- apps/dokploy/pages/dashboard/swarm.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index d8b6b0615..2722e0e46 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -15,10 +15,6 @@ const Dashboard = () => {
- -
- -
); }; From 5a302d3c47498b0321f470739a6ca88aa19ac84c Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Sun, 22 Dec 2024 15:13:04 -0500 Subject: [PATCH 41/85] feat(logs): use fancy-ansi for ansi colors --- .../dashboard/docker/logs/terminal-line.tsx | 68 ++++++++------ .../components/dashboard/docker/logs/utils.ts | 94 ------------------- apps/dokploy/package.json | 5 +- apps/dokploy/tailwind.config.ts | 2 +- pnpm-lock.yaml | 17 ++++ 5 files changed, 59 insertions(+), 127 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 2f247e259..c25acc67f 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -7,9 +7,10 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; +import { FancyAnsi } from "fancy-ansi"; import { escapeRegExp } from "lodash"; import React from "react"; -import { type LogLine, getLogType, parseAnsi } from "./utils"; +import { type LogLine, getLogType } from "./utils"; interface LogLineProps { log: LogLine; @@ -17,6 +18,8 @@ interface LogLineProps { searchTerm?: string; } +const fancyAnsi = new FancyAnsi(); + export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { const { timestamp, message, rawTimestamp } = log; const { type, variant, color } = getLogType(message); @@ -34,37 +37,42 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { const highlightMessage = (text: string, term: string) => { if (!term) { - const segments = parseAnsi(text); - return segments.map((segment, index) => ( - - {segment.text} - - )); - } - - // For search, we need to handle both ANSI and search highlighting - const segments = parseAnsi(text); - return segments.map((segment, index) => { - const parts = segment.text.split( - new RegExp(`(${escapeRegExp(term)})`, "gi"), - ); return ( - - {parts.map((part, partIndex) => - part.toLowerCase() === term.toLowerCase() ? ( - - {part} - - ) : ( - part - ), - )} - + ); - }); + } + + const htmlContent = fancyAnsi.toHtml(text); + const modifiedContent = htmlContent.replace( + /]*)>([^<]*)<\/span>/g, + (match, attrs, content) => { + const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); + if (!content.match(searchRegex)) return match; + + const segments = content.split(searchRegex); + const wrappedSegments = segments + .map((segment: string) => + segment.toLowerCase() === term.toLowerCase() + ? `${segment}` + : segment, + ) + .join(""); + + return `${wrappedSegments}`; + }, + ); + + return ( + + ); }; const tooltip = (color: string, timestamp: string | null) => { diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 482194287..cf0b30bbd 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -12,47 +12,6 @@ interface LogStyle { variant: LogVariant; color: string; } -interface AnsiSegment { - text: string; - className: string; -} - -const ansiToTailwind: Record = { - // Reset - 0: "", - // Regular colors - 30: "text-black dark:text-gray-900", - 31: "text-red-600 dark:text-red-500", - 32: "text-green-600 dark:text-green-500", - 33: "text-yellow-600 dark:text-yellow-500", - 34: "text-blue-600 dark:text-blue-500", - 35: "text-purple-600 dark:text-purple-500", - 36: "text-cyan-600 dark:text-cyan-500", - 37: "text-gray-600 dark:text-gray-400", - // Bright colors - 90: "text-gray-500 dark:text-gray-600", - 91: "text-red-500 dark:text-red-600", - 92: "text-green-500 dark:text-green-600", - 93: "text-yellow-500 dark:text-yellow-600", - 94: "text-blue-500 dark:text-blue-600", - 95: "text-purple-500 dark:text-purple-600", - 96: "text-cyan-500 dark:text-cyan-600", - 97: "text-white dark:text-gray-300", - // Background colors - 40: "bg-black", - 41: "bg-red-600", - 42: "bg-green-600", - 43: "bg-yellow-600", - 44: "bg-blue-600", - 45: "bg-purple-600", - 46: "bg-cyan-600", - 47: "bg-white", - // Formatting - 1: "font-bold", - 2: "opacity-75", - 3: "italic", - 4: "underline", -}; const LOG_STYLES: Record = { error: { @@ -191,56 +150,3 @@ export const getLogType = (message: string): LogStyle => { return LOG_STYLES.info; }; - -export function parseAnsi(text: string) { - const segments: { text: string; className: string }[] = []; - let currentIndex = 0; - let currentClasses: string[] = []; - - while (currentIndex < text.length) { - const escStart = text.indexOf("\x1b[", currentIndex); - - // No more escape sequences found - if (escStart === -1) { - if (currentIndex < text.length) { - segments.push({ - text: text.slice(currentIndex), - className: currentClasses.join(" "), - }); - } - break; - } - - // Add text before escape sequence - if (escStart > currentIndex) { - segments.push({ - text: text.slice(currentIndex, escStart), - className: currentClasses.join(" "), - }); - } - - const escEnd = text.indexOf("m", escStart); - if (escEnd === -1) break; - - // Handle multiple codes in one sequence (e.g., \x1b[1;31m) - const codesStr = text.slice(escStart + 2, escEnd); - const codes = codesStr.split(";").map((c) => Number.parseInt(c, 10)); - - if (codes.includes(0)) { - // Reset all formatting - currentClasses = []; - } else { - // Add new classes for each code - for (const code of codes) { - const className = ansiToTailwind[code]; - if (className && !currentClasses.includes(className)) { - currentClasses.push(className); - } - } - } - - currentIndex = escEnd + 1; - } - - return segments; -} \ No newline at end of file diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 39c075a66..a16dedacc 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -35,8 +35,6 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { - "react-confetti-explosion":"2.1.2", - "@stepperize/react": "4.0.1", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.1", @@ -64,6 +62,7 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", + "@stepperize/react": "4.0.1", "@stripe/stripe-js": "4.8.0", "@tanstack/react-query": "^4.36.1", "@tanstack/react-table": "^8.16.0", @@ -87,6 +86,7 @@ "dotenv": "16.4.5", "drizzle-orm": "^0.30.8", "drizzle-zod": "0.5.1", + "fancy-ansi": "^0.1.3", "i18next": "^23.16.4", "input-otp": "^1.2.4", "js-cookie": "^3.0.5", @@ -104,6 +104,7 @@ "postgres": "3.4.4", "public-ip": "6.0.2", "react": "18.2.0", + "react-confetti-explosion": "2.1.2", "react-dom": "18.2.0", "react-hook-form": "^7.49.3", "react-i18next": "^15.1.0", diff --git a/apps/dokploy/tailwind.config.ts b/apps/dokploy/tailwind.config.ts index c4fa88ecd..45b529afd 100644 --- a/apps/dokploy/tailwind.config.ts +++ b/apps/dokploy/tailwind.config.ts @@ -87,7 +87,7 @@ const config = { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [require("tailwindcss-animate"), require("fancy-ansi/plugin")], } satisfies Config; export default config; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62ba2f564..df2dc8e03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -250,6 +250,9 @@ importers: drizzle-zod: specifier: 0.5.1 version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8) + fancy-ansi: + specifier: ^0.1.3 + version: 0.1.3 i18next: specifier: ^23.16.4 version: 23.16.5 @@ -874,9 +877,11 @@ packages: '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -4401,6 +4406,9 @@ packages: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -4460,6 +4468,9 @@ packages: ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + fancy-ansi@0.1.3: + resolution: {integrity: sha512-tRQVTo5jjdSIiydqgzIIEZpKddzSsfGLsSVt6vWdjVm7fbvDTiQkyoPu6Z3dIPlAM4OZk0jP5jmTCX4G8WGgBw==} + fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} @@ -10735,6 +10746,8 @@ snapshots: escalade@3.1.2: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@5.0.0: {} @@ -10795,6 +10808,10 @@ snapshots: dependencies: type: 2.7.3 + fancy-ansi@0.1.3: + dependencies: + escape-html: 1.0.3 + fast-copy@3.0.2: {} fast-deep-equal@2.0.1: {} From 7a5b9e3b76946725dbd57e7369aac060d51444f8 Mon Sep 17 00:00:00 2001 From: 190km Date: Mon, 23 Dec 2024 02:34:50 +0100 Subject: [PATCH 42/85] refactor: deleted separate component, and add it to show preview build --- .../preview-deployment-card.tsx | 106 ------ .../show-preview-deployments.tsx | 306 +++++++++++++----- 2 files changed, 226 insertions(+), 186 deletions(-) delete mode 100644 apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx deleted file mode 100644 index 4d61fdec9..000000000 --- a/apps/dokploy/components/dashboard/application/preview-deployments/preview-deployment-card.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Separator } from "@/components/ui/separator"; -import { Clock, GitBranch, GitPullRequest } from "lucide-react"; -import Link from "next/link"; -import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; -import { DialogAction } from "@/components/shared/dialog-action"; -import { api } from "@/utils/api"; -import { ShowPreviewBuilds } from "./show-preview-builds"; -import { DateTooltip } from "@/components/shared/date-tooltip"; - -interface PreviewDeploymentCardProps { - deploymentId: string; - serverId: string; - onDeploymentDelete: (deploymentId: string) => void; - isLoading: boolean; -} - -export function PreviewDeploymentCard({ - deploymentId, - serverId, - onDeploymentDelete, - isLoading, -}: PreviewDeploymentCardProps) { - const { data: previewDeployment } = api.previewDeployment.one.useQuery({ - previewDeploymentId: deploymentId, - }); - - if (!previewDeployment) return null; - - return ( -
-
- {previewDeployment.pullRequestTitle} - - {previewDeployment.previewStatus || "Idle"} - -
-
-
- - {previewDeployment.domain?.host} - -
-
-
- - Branch: - - {previewDeployment.branch} - -
-
- - Deployed: - - - -
-
- -
-

Pull Request

-
- - - {previewDeployment.pullRequestTitle} - -
-
-
-
-
- - - - - onDeploymentDelete(deploymentId)} - > - - -
-
-
- ); -} \ No newline at end of file diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index fbcc9a728..2ce187319 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,93 +1,239 @@ +import React from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; + Clock, + GitBranch, + GitPullRequest, + Pencil, + RocketIcon, +} from "lucide-react"; +import Link from "next/link"; +import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; +import { DialogAction } from "@/components/shared/dialog-action"; import { api } from "@/utils/api"; -import { RocketIcon } from "lucide-react"; -import React from "react"; +import { ShowPreviewBuilds } from "./show-preview-builds"; +import { DateTooltip } from "@/components/shared/date-tooltip"; import { toast } from "sonner"; -import { PreviewDeploymentCard } from "./preview-deployment-card"; +import { StatusTooltip } from "@/components/shared/status-tooltip"; +import { AddPreviewDomain } from "./add-preview-domain"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { ShowPreviewSettings } from "./show-preview-settings"; interface Props { - applicationId: string; + applicationId: string; } export const ShowPreviewDeployments = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery({ applicationId }); + const { data } = api.application.one.useQuery({ applicationId }); + + const { mutateAsync: deletePreviewDeployment, isLoading } = + api.previewDeployment.delete.useMutation(); + + const { data: previewDeployments, refetch: refetchPreviewDeployments } = + api.previewDeployment.all.useQuery( + { applicationId }, + { + enabled: !!applicationId, + } + ); + + const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { + deletePreviewDeployment({ + previewDeploymentId: previewDeploymentId, + }) + .then(() => { + refetchPreviewDeployments(); + toast.success("Preview deployment deleted"); + }) + .catch((error) => { + toast.error(error.message); + }); + }; - const { mutateAsync: deletePreviewDeployment, isLoading } = - api.previewDeployment.delete.useMutation(); - const { data: previewDeployments, refetch: refetchPreviewDeployments } = - api.previewDeployment.all.useQuery( - { applicationId }, - { - enabled: !!applicationId, - } - ); + return ( + + +
+ Preview Deployments + See all the preview deployments +
+ {data?.isPreviewDeploymentsActive && ( + + )} +
+ + {data?.isPreviewDeploymentsActive ? ( + <> +
+ + Preview deployments are a way to test your application before it + is deployed to production. It will create a new deployment for + each pull request you create. + +
+ {!previewDeployments?.length ? ( +
+ + + No preview deployments found + +
+ ) : ( +
+ {previewDeployments.map((previewDeployment) => ( +
+
+ + {previewDeployment.pullRequestTitle} + + + + {previewDeployment.previewStatus + ?.replace("running", "Running") + .replace("done", "Done") + .replace("error", "Error") + .replace("idle", "Idle") || "Idle"} + +
- const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { - deletePreviewDeployment({ - previewDeploymentId: previewDeploymentId, - }) - .then(() => { - refetchPreviewDeployments(); - toast.success("Preview deployment deleted"); - }) - .catch((error) => { - toast.error(error.message); - }); - }; +
+
+ + {previewDeployment.domain?.host} + - return ( - - -
- Preview Deployments - See all the preview deployments -
- {data?.isPreviewDeploymentsActive && ( - - )} -
- - {data?.isPreviewDeploymentsActive ? ( - <> - {data?.previewDeployments?.length === 0 ? ( -
- - - No preview deployments found - -
- ) : ( -
- {previewDeployments?.map((previewDeployment) => ( - - ))} -
- )} - - ) : ( -
- - - Preview deployments are disabled for this application, please - enable it - - -
- )} -
-
- ); + + + +
+ +
+
+ + Branch: + + {previewDeployment.branch} + +
+
+ + Deployed: + + + +
+
+ + + +
+

+ Pull Request +

+
+ + + {previewDeployment.pullRequestTitle} + +
+
+
+ +
+
+ + + + + + + + handleDeletePreviewDeployment( + previewDeployment.previewDeploymentId + ) + } + > + + +
+
+
+ ))} +
+ )} + + ) : ( +
+ + + Preview deployments are disabled for this application, please + enable it + + +
+ )} +
+
+ ); }; + +export default ShowPreviewDeployments; From 65ddc22010cb63d0bed5ccaabf8a665d9926f3ac Mon Sep 17 00:00:00 2001 From: 190km Date: Mon, 23 Dec 2024 02:41:08 +0100 Subject: [PATCH 43/85] refactor: removed useless export --- .../preview-deployments/show-preview-deployments.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 2ce187319..898d412a0 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -235,5 +235,3 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { ); }; - -export default ShowPreviewDeployments; From eb495b7b9973703e4ded71ee663a243dfaf3ed8e Mon Sep 17 00:00:00 2001 From: 190km Date: Mon, 23 Dec 2024 03:15:11 +0100 Subject: [PATCH 44/85] style: fixed white style --- .../dashboard/settings/notifications/show-notifications.tsx | 4 ++-- .../dashboard/settings/notifications/update-notification.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 10ea7304e..2b5d02e0e 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -44,7 +44,7 @@ export const ShowNotifications = () => { {data?.map((notification, index) => (
{notification.notificationType === "slack" && ( @@ -68,7 +68,7 @@ export const ShowNotifications = () => {
)}
- + {notification.name} diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx index cfa2e0bab..5c594dc49 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx @@ -220,7 +220,7 @@ export const UpdateNotification = ({ notificationId }: Props) => { From 49616e53ea1de127d1370f3de5ff7e92d6e2f30e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 04:45:59 +0000 Subject: [PATCH 45/85] fix: prevent Esc key from closing terminal modals Co-Authored-By: Nicholas Penree (cherry picked from commit ee469d5ac3f72cc8e010728cc7c6273c711875b0) --- .../dashboard/docker/terminal/docker-terminal-modal.tsx | 7 +++++-- .../settings/web-server/docker-terminal-modal.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx index c3dba4f51..e7301b0aa 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx @@ -59,7 +59,10 @@ export const DockerTerminalModal = ({ {children} - + event.preventDefault()} + > Docker Terminal @@ -73,7 +76,7 @@ export const DockerTerminalModal = ({ serverId={serverId || ""} /> - + event.preventDefault()}> Are you sure you want to close the terminal? diff --git a/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx index f81be0adf..f5b6b2a9e 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx @@ -80,7 +80,10 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => { return ( {children} - + event.preventDefault()} + > Docker Terminal @@ -119,7 +122,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => { containerId={containerId || "select-a-container"} /> - + event.preventDefault()}> Are you sure you want to close the terminal? From 6afd443257d9d94a355b799d9319d51107e9111b Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:03:30 -0600 Subject: [PATCH 46/85] feat: add swarm overview for servers --- .../settings/servers/show-servers.tsx | 4 + .../servers/show-swarm-overview-modal.tsx | 51 +++++++++ .../swarm/applications/show-applications.tsx | 25 +++-- .../dashboard/swarm/details/details-card.tsx | 38 ++++--- .../swarm/details/show-node-config.tsx | 8 +- .../dashboard/swarm/monitoring-card.tsx | 82 ++++++-------- .../dashboard/swarm/servers/server-card.tsx | 103 ------------------ .../swarm/servers/servers-overview.tsx | 74 ------------- .../components/layouts/navigation-tabs.tsx | 2 +- apps/dokploy/pages/dashboard/swarm.tsx | 2 - apps/dokploy/server/api/routers/swarm.ts | 31 ++++-- packages/server/src/services/docker.ts | 75 ++++++++++--- 12 files changed, 210 insertions(+), 285 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/server-card.tsx delete mode 100644 apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx index a174cd9cb..d476fb15f 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -33,6 +33,7 @@ import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { UpdateServer } from "./update-server"; import { useRouter } from "next/router"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; +import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; export const ShowServers = () => { const router = useRouter(); @@ -259,6 +260,9 @@ export const ShowServers = () => { + )} diff --git a/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx b/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx new file mode 100644 index 000000000..f8acd207d --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/show-swarm-overview-modal.tsx @@ -0,0 +1,51 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { ContainerIcon } from "lucide-react"; +import { useState } from "react"; +import SwarmMonitorCard from "../../swarm/monitoring-card"; + +interface Props { + serverId: string; +} + +export const ShowSwarmOverviewModal = ({ serverId }: Props) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + e.preventDefault()} + > + Show Swarm Overview + + + + +
+ + + Swarm Overview + +

+ See all details of your swarm node +

+
+
+ +
+
+ +
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx index e3b38a71d..132cb0081 100644 --- a/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx +++ b/apps/dokploy/components/dashboard/swarm/applications/show-applications.tsx @@ -8,13 +8,13 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { Layers, LoaderIcon } from "lucide-react"; +import { Layers, Loader2 } from "lucide-react"; import React from "react"; import { columns } from "./columns"; import { DataTable } from "./data-table"; interface Props { - nodeName: string; + serverId?: string; } interface ApplicationList { @@ -30,10 +30,9 @@ interface ApplicationList { Node: string; } -const ShowNodeApplications = ({ nodeName }: Props) => { - const [loading, setLoading] = React.useState(true); +export const ShowNodeApplications = ({ serverId }: Props) => { const { data: NodeApps, isLoading: NodeAppsLoading } = - api.swarm.getNodeApps.useQuery(); + api.swarm.getNodeApps.useQuery({ serverId }); let applicationList = ""; @@ -42,14 +41,14 @@ const ShowNodeApplications = ({ nodeName }: Props) => { } const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } = - api.swarm.getAppInfos.useQuery({ appName: applicationList }); + api.swarm.getAppInfos.useQuery({ appName: applicationList, serverId }); if (NodeAppsLoading || NodeAppDetailsLoading) { return ( @@ -57,7 +56,11 @@ const ShowNodeApplications = ({ nodeName }: Props) => { } if (!NodeApps || !NodeAppDetails) { - return
No data found
; + return ( + + No data found + + ); } const combinedData: ApplicationList[] = NodeApps.flatMap((app) => { @@ -97,19 +100,17 @@ const ShowNodeApplications = ({ nodeName }: Props) => { Services - + Node Applications See in detail the applications running on this node -
+
); }; - -export default ShowNodeApplications; diff --git a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx index b8eb9f81f..a499f898f 100644 --- a/apps/dokploy/components/dashboard/swarm/details/details-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/details-card.tsx @@ -1,9 +1,14 @@ import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { AlertCircle, CheckCircle, HelpCircle, LoaderIcon } from "lucide-react"; -import { useState } from "react"; -import ShowNodeApplications from "../applications/show-applications"; +import { + AlertCircle, + CheckCircle, + HelpCircle, + Loader2, + LoaderIcon, +} from "lucide-react"; +import { ShowNodeApplications } from "../applications/show-applications"; import { ShowNodeConfig } from "./show-node-config"; export interface SwarmList { @@ -16,13 +21,15 @@ export interface SwarmList { TLSStatus: string; } -interface NodeCardProps { +interface Props { node: SwarmList; + serverId?: string; } -export function NodeCard({ node }: NodeCardProps) { +export function NodeCard({ node, serverId }: Props) { const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId: node.ID, + serverId, }); const getStatusIcon = (status: string) => { @@ -40,7 +47,7 @@ export function NodeCard({ node }: NodeCardProps) { return ( - + {getStatusIcon(node.Status)} {node.Hostname} @@ -52,7 +59,7 @@ export function NodeCard({ node }: NodeCardProps) {
- +
@@ -63,7 +70,7 @@ export function NodeCard({ node }: NodeCardProps) { - + {getStatusIcon(node.Status)} {node.Hostname} @@ -83,7 +90,7 @@ export function NodeCard({ node }: NodeCardProps) { {isLoading ? ( ) : ( - {data.Status.Addr} + {data?.Status?.Addr} )}
@@ -100,7 +107,7 @@ export function NodeCard({ node }: NodeCardProps) { ) : ( - {(data.Description.Resources.NanoCPUs / 1e9).toFixed(2)} GHz + {(data?.Description?.Resources?.NanoCPUs / 1e9).toFixed(2)} GHz )}
@@ -110,9 +117,10 @@ export function NodeCard({ node }: NodeCardProps) { ) : ( - {(data.Description.Resources.MemoryBytes / 1024 ** 3).toFixed( - 2, - )}{" "} + {( + data?.Description?.Resources?.MemoryBytes / + 1024 ** 3 + ).toFixed(2)}{" "} GB )} @@ -123,8 +131,8 @@ export function NodeCard({ node }: NodeCardProps) {
- - + +
diff --git a/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx index 2d8a3e3ee..a41c5a497 100644 --- a/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx +++ b/apps/dokploy/components/dashboard/swarm/details/show-node-config.tsx @@ -13,10 +13,14 @@ import { Settings } from "lucide-react"; interface Props { nodeId: string; + serverId?: string; } -export const ShowNodeConfig = ({ nodeId }: Props) => { - const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ nodeId }); +export const ShowNodeConfig = ({ nodeId, serverId }: Props) => { + const { data, isLoading } = api.swarm.getNodeInfo.useQuery({ + nodeId, + serverId, + }); return ( diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 81a681721..e2453dd9f 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -9,68 +9,44 @@ import { } from "@/components/ui/tooltip"; import { api } from "@/utils/api"; import { - Activity, AlertCircle, CheckCircle, HelpCircle, Loader2, Server, } from "lucide-react"; -import Link from "next/link"; import { NodeCard } from "./details/details-card"; -export interface SwarmList { - ID: string; - Hostname: string; - Availability: string; - EngineVersion: string; - Status: string; - ManagerStatus: string; - TLSStatus: string; +interface Props { + serverId?: string; } -interface SwarmMonitorCardProps { - nodes: SwarmList[]; -} - -export default function SwarmMonitorCard() { - const { data: nodes, isLoading } = api.swarm.getNodes.useQuery(); +export default function SwarmMonitorCard({ serverId }: Props) { + const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({ + serverId, + }); if (isLoading) { return (
- - - - - Docker Swarm Monitor - - - -
- -
-
-
+
+
+ +
+
); } if (!nodes) { return ( - - - - - Docker Swarm Monitor - - - -
+
+
+
Failed to load data
- - +
+
); } @@ -105,19 +81,23 @@ export default function SwarmMonitorCard() { return (
-

Docker Swarm Overview

- +

Docker Swarm Overview

+ {!serverId && ( + + )}
- - - Docker Swarm Monitor + + + Monitor @@ -200,7 +180,7 @@ export default function SwarmMonitorCard() {
{nodes.map((node) => ( - + ))}
diff --git a/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx b/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx deleted file mode 100644 index 4b732df4a..000000000 --- a/apps/dokploy/components/dashboard/swarm/servers/server-card.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { AlertCircle, CheckCircle, HelpCircle, ServerIcon } from "lucide-react"; -import { ShowContainers } from "../../docker/show/show-containers"; - -export interface Server { - serverId: string; - name: string; - description: string | null; - ipAddress: string; - port: number; - username: string; - appName: string; - enableDockerCleanup: boolean; - createdAt: string; - adminId: string; - serverStatus: "active" | "inactive"; - command: string; - sshKeyId: string | null; -} - -interface ServerOverviewCardProps { - server: Server; -} - -export function ServerOverviewCard({ server }: ServerOverviewCardProps) { - const getStatusIcon = (status: string) => { - switch (status) { - case "active": - return ; - case "inactive": - return ; - default: - return ; - } - }; - - return ( - - - - - {getStatusIcon(server.serverStatus)} - {server.name} - - - {server.serverStatus} - - - - -
-
- IP Address: - {server.ipAddress} -
-
- Port: - {server.port} -
-
- Username: - {server.username} -
-
- App Name: - {server.appName} -
-
- Docker Cleanup: - {server.enableDockerCleanup ? "Enabled" : "Disabled"} -
-
- Created At: - {new Date(server.createdAt).toLocaleString()} -
-
-
- - - - - - - - -
-
-
- ); -} diff --git a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx b/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx deleted file mode 100644 index bd54f43ef..000000000 --- a/apps/dokploy/components/dashboard/swarm/servers/servers-overview.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; -import { api } from "@/utils/api"; -import { LoaderIcon } from "lucide-react"; -import { ServerOverviewCard } from "./server-card"; - -export default function ServersOverview() { - const { data: servers, isLoading } = api.server.all.useQuery(); - - if (isLoading) { - return ( - <> - - - - - - - - - - - - -
-
- IP Address: -
-
- Port: -
-
- Username: -
-
- App Name: -
-
- Docker Cleanup: -
-
- Created At: -
-
-
-
- - ); - } - - if (!servers) { - return
No servers found
; - } - return ( -
-
-

Server Overview

- -
-
- {servers.map((server) => ( - - ))} -
-
- ); -} diff --git a/apps/dokploy/components/layouts/navigation-tabs.tsx b/apps/dokploy/components/layouts/navigation-tabs.tsx index 46e590a73..782b9d467 100644 --- a/apps/dokploy/components/layouts/navigation-tabs.tsx +++ b/apps/dokploy/components/layouts/navigation-tabs.tsx @@ -62,7 +62,7 @@ const getTabMaps = (isCloud: boolean) => { type: "docker", }, { - label: "Swarm & Server", + label: "Swarm", description: "Manage your docker swarm and Servers", index: "/dashboard/swarm", isShow: ({ rol, user }) => { diff --git a/apps/dokploy/pages/dashboard/swarm.tsx b/apps/dokploy/pages/dashboard/swarm.tsx index 2722e0e46..15a7d7937 100644 --- a/apps/dokploy/pages/dashboard/swarm.tsx +++ b/apps/dokploy/pages/dashboard/swarm.tsx @@ -1,7 +1,5 @@ import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card"; -import ServersOverview from "@/components/dashboard/swarm/servers/servers-overview"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { Separator } from "@/components/ui/separator"; import { appRouter } from "@/server/api/root"; import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { createServerSideHelpers } from "@trpc/react-query/server"; diff --git a/apps/dokploy/server/api/routers/swarm.ts b/apps/dokploy/server/api/routers/swarm.ts index fe15d0ef3..c5a2d4c82 100644 --- a/apps/dokploy/server/api/routers/swarm.ts +++ b/apps/dokploy/server/api/routers/swarm.ts @@ -8,24 +8,37 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const swarmRouter = createTRPCRouter({ - getNodes: protectedProcedure.query(async () => { - return await getSwarmNodes(); - }), + getNodes: protectedProcedure + .input( + z.object({ + serverId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + return await getSwarmNodes(input.serverId); + }), getNodeInfo: protectedProcedure - .input(z.object({ nodeId: z.string() })) + .input(z.object({ nodeId: z.string(), serverId: z.string().optional() })) + .query(async ({ input }) => { + return await getNodeInfo(input.nodeId, input.serverId); + }), + getNodeApps: protectedProcedure + .input( + z.object({ + serverId: z.string().optional(), + }), + ) .query(async ({ input }) => { - return await getNodeInfo(input.nodeId); + return getNodeApplications(input.serverId); }), - getNodeApps: protectedProcedure.query(async () => { - return getNodeApplications(); - }), getAppInfos: protectedProcedure .input( z.object({ appName: z.string(), + serverId: z.string().optional(), }), ) .query(async ({ input }) => { - return await getApplicationInfo(input.appName); + return await getApplicationInfo(input.appName, input.serverId); }), }); diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index c13c71e5f..60262ba13 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -225,11 +225,21 @@ export const containerRestart = async (containerId: string) => { } catch (error) {} }; -export const getSwarmNodes = async () => { +export const getSwarmNodes = async (serverId?: string) => { try { - const { stdout, stderr } = await execAsync( - "docker node ls --format '{{json .}}'", - ); + let stdout = ""; + let stderr = ""; + const command = "docker node ls --format '{{json .}}'"; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`); @@ -246,11 +256,20 @@ export const getSwarmNodes = async () => { } catch (error) {} }; -export const getNodeInfo = async (nodeId: string) => { +export const getNodeInfo = async (nodeId: string, serverId?: string) => { try { - const { stdout, stderr } = await execAsync( - `docker node inspect ${nodeId} --format '{{json .}}'`, - ); + const command = `docker node inspect ${nodeId} --format '{{json .}}'`; + let stdout = ""; + let stderr = ""; + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`); @@ -263,11 +282,22 @@ export const getNodeInfo = async (nodeId: string) => { } catch (error) {} }; -export const getNodeApplications = async () => { +export const getNodeApplications = async (serverId?: string) => { try { - const { stdout, stderr } = await execAsync( - `docker service ls --format '{{json .}}'`, - ); + let stdout = ""; + let stderr = ""; + const command = `docker service ls --format '{{json .}}'`; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`); @@ -283,11 +313,24 @@ export const getNodeApplications = async () => { } catch (error) {} }; -export const getApplicationInfo = async (appName: string) => { +export const getApplicationInfo = async ( + appName: string, + serverId?: string, +) => { try { - const { stdout, stderr } = await execAsync( - `docker service ps ${appName} --format '{{json .}}'`, - ); + let stdout = ""; + let stderr = ""; + const command = `docker service ps ${appName} --format '{{json .}}'`; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } if (stderr) { console.error(`Error: ${stderr}`); From be2e70a17ebf3540a5b225161870480bf952d63c Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Mon, 23 Dec 2024 08:27:45 +0100 Subject: [PATCH 47/85] chore: only make this apply to compose files --- packages/server/src/services/compose.ts | 929 ++++++++++++------------ 1 file changed, 466 insertions(+), 463 deletions(-) diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 385e553a0..9012cee6c 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -5,42 +5,42 @@ import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { - buildCompose, - getBuildComposeCommand, + buildCompose, + getBuildComposeCommand, } from "@dokploy/server/utils/builders/compose"; import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose"; import { - cloneCompose, - cloneComposeRemote, - loadDockerCompose, - loadDockerComposeRemote, + cloneCompose, + cloneComposeRemote, + loadDockerCompose, + loadDockerComposeRemote, } from "@dokploy/server/utils/docker/domain"; import type { ComposeSpecification } from "@dokploy/server/utils/docker/types"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { - execAsync, - execAsyncRemote, + execAsync, + execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; import { - cloneBitbucketRepository, - getBitbucketCloneCommand, + cloneBitbucketRepository, + getBitbucketCloneCommand, } from "@dokploy/server/utils/providers/bitbucket"; import { - cloneGitRepository, - getCustomGitCloneCommand, + cloneGitRepository, + getCustomGitCloneCommand, } from "@dokploy/server/utils/providers/git"; import { - cloneGithubRepository, - getGithubCloneCommand, + cloneGithubRepository, + getGithubCloneCommand, } from "@dokploy/server/utils/providers/github"; import { - cloneGitlabRepository, - getGitlabCloneCommand, + cloneGitlabRepository, + getGitlabCloneCommand, } from "@dokploy/server/utils/providers/gitlab"; import { - createComposeFile, - getCreateComposeFileCommand, + createComposeFile, + getCreateComposeFileCommand, } from "@dokploy/server/utils/providers/raw"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; @@ -52,498 +52,501 @@ import { validUniqueServerAppName } from "./project"; export type Compose = typeof compose.$inferSelect; export const createCompose = async (input: typeof apiCreateCompose._type) => { - const appName = buildAppName("compose", input.appName); - - const valid = await validUniqueServerAppName(appName); - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } - - const newDestination = await db - .insert(compose) - .values({ - ...input, - composeFile: "", - appName, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting compose", - }); - } - - return newDestination; + const appName = buildAppName("compose", input.appName); + + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + + const newDestination = await db + .insert(compose) + .values({ + ...input, + composeFile: "", + appName, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting compose", + }); + } + + return newDestination; }; export const createComposeByTemplate = async ( - input: typeof compose.$inferInsert, + input: typeof compose.$inferInsert ) => { - const appName = cleanAppName(input.appName); - if (appName) { - const valid = await validUniqueServerAppName(appName); - - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } - } - const newDestination = await db - .insert(compose) - .values({ - ...input, - appName, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting compose", - }); - } - - return newDestination; + const appName = cleanAppName(input.appName); + if (appName) { + const valid = await validUniqueServerAppName(appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newDestination = await db + .insert(compose) + .values({ + ...input, + appName, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting compose", + }); + } + + return newDestination; }; export const findComposeById = async (composeId: string) => { - const result = await db.query.compose.findFirst({ - where: eq(compose.composeId, composeId), - with: { - project: true, - deployments: true, - mounts: true, - domains: true, - github: true, - gitlab: true, - bitbucket: true, - server: true, - }, - }); - if (!result) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Compose not found", - }); - } - return result; + const result = await db.query.compose.findFirst({ + where: eq(compose.composeId, composeId), + with: { + project: true, + deployments: true, + mounts: true, + domains: true, + github: true, + gitlab: true, + bitbucket: true, + server: true, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Compose not found", + }); + } + return result; }; export const loadServices = async ( - composeId: string, - type: "fetch" | "cache" = "fetch", + composeId: string, + type: "fetch" | "cache" = "fetch" ) => { - const compose = await findComposeById(composeId); - - if (type === "fetch") { - if (compose.serverId) { - await cloneComposeRemote(compose); - } else { - await cloneCompose(compose); - } - } - - let composeData: ComposeSpecification | null; - - if (compose.serverId) { - composeData = await loadDockerComposeRemote(compose); - } else { - composeData = await loadDockerCompose(compose); - } - - if (compose.randomize && composeData) { - const randomizedCompose = randomizeSpecificationFile( - composeData, - compose.suffix, - ); - composeData = randomizedCompose; - } - - if (!composeData?.services) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Services not found", - }); - } - - const services = Object.keys(composeData.services); - - return [...services]; + const compose = await findComposeById(composeId); + + if (type === "fetch") { + if (compose.serverId) { + await cloneComposeRemote(compose); + } else { + await cloneCompose(compose); + } + } + + let composeData: ComposeSpecification | null; + + if (compose.serverId) { + composeData = await loadDockerComposeRemote(compose); + } else { + composeData = await loadDockerCompose(compose); + } + + if (compose.randomize && composeData) { + const randomizedCompose = randomizeSpecificationFile( + composeData, + compose.suffix + ); + composeData = randomizedCompose; + } + + if (!composeData?.services) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Services not found", + }); + } + + const services = Object.keys(composeData.services); + + return [...services]; }; export const updateCompose = async ( - composeId: string, - composeData: Partial, + composeId: string, + composeData: Partial ) => { - const { appName, ...rest } = composeData; - const composeResult = await db - .update(compose) - .set({ - ...rest, - }) - .where(eq(compose.composeId, composeId)) - .returning(); - - return composeResult[0]; + const { appName, ...rest } = composeData; + const composeResult = await db + .update(compose) + .set({ + ...rest, + }) + .where(eq(compose.composeId, composeId)) + .returning(); + + return composeResult[0]; }; export const deployCompose = async ({ - composeId, - titleLog = "Manual deployment", - descriptionLog = "", + composeId, + titleLog = "Manual deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`; - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.sourceType === "github") { - await cloneGithubRepository({ - ...compose, - logPath: deployment.logPath, - type: "compose", - }); - } else if (compose.sourceType === "gitlab") { - await cloneGitlabRepository(compose, deployment.logPath, true); - } else if (compose.sourceType === "bitbucket") { - await cloneBitbucketRepository(compose, deployment.logPath, true); - } else if (compose.sourceType === "git") { - await cloneGitRepository(compose, deployment.logPath, true); - } else if (compose.sourceType === "raw") { - await createComposeFile(compose, deployment.logPath); - } - await buildCompose(compose, deployment.logPath); - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - - await sendBuildSuccessNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - buildLink, - adminId: compose.project.adminId, - }); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - await sendBuildErrorNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - // @ts-ignore - errorMessage: error?.message || "Error to build", - buildLink, - adminId: compose.project.adminId, - }); - throw error; - } + const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${ + compose.projectId + }/services/compose/${compose.composeId}?tab=deployments`; + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.sourceType === "github") { + await cloneGithubRepository({ + ...compose, + logPath: deployment.logPath, + type: "compose", + }); + } else if (compose.sourceType === "gitlab") { + await cloneGitlabRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "bitbucket") { + await cloneBitbucketRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "git") { + await cloneGitRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "raw") { + await createComposeFile(compose, deployment.logPath); + } + await buildCompose(compose, deployment.logPath); + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + + await sendBuildSuccessNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + buildLink, + adminId: compose.project.adminId, + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + await sendBuildErrorNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + adminId: compose.project.adminId, + }); + throw error; + } }; export const rebuildCompose = async ({ - composeId, - titleLog = "Rebuild deployment", - descriptionLog = "", + composeId, + titleLog = "Rebuild deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.serverId) { - await getBuildComposeCommand(compose, deployment.logPath); - } else { - await buildCompose(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; + const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.serverId) { + await getBuildComposeCommand(compose, deployment.logPath); + } else { + await buildCompose(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; }; export const deployRemoteCompose = async ({ - composeId, - titleLog = "Manual deployment", - descriptionLog = "", + composeId, + titleLog = "Manual deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`; - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - try { - if (compose.serverId) { - let command = "set -e;"; - - if (compose.sourceType === "github") { - command += await getGithubCloneCommand({ - ...compose, - logPath: deployment.logPath, - type: "compose", - serverId: compose.serverId, - }); - } else if (compose.sourceType === "gitlab") { - command += await getGitlabCloneCommand( - compose, - deployment.logPath, - true, - ); - } else if (compose.sourceType === "bitbucket") { - command += await getBitbucketCloneCommand( - compose, - deployment.logPath, - true, - ); - } else if (compose.sourceType === "git") { - command += await getCustomGitCloneCommand( - compose, - deployment.logPath, - true, - ); - } else if (compose.sourceType === "raw") { - command += getCreateComposeFileCommand(compose, deployment.logPath); - } - - await execAsyncRemote(compose.serverId, command); - await getBuildComposeCommand(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - - await sendBuildSuccessNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - buildLink, - adminId: compose.project.adminId, - }); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - compose.serverId, - ` + const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${ + compose.projectId + }/services/compose/${compose.composeId}?tab=deployments`; + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + try { + if (compose.serverId) { + let command = "set -e;"; + + if (compose.sourceType === "github") { + command += await getGithubCloneCommand({ + ...compose, + logPath: deployment.logPath, + type: "compose", + serverId: compose.serverId, + }); + } else if (compose.sourceType === "gitlab") { + command += await getGitlabCloneCommand( + compose, + deployment.logPath, + true + ); + } else if (compose.sourceType === "bitbucket") { + command += await getBitbucketCloneCommand( + compose, + deployment.logPath, + true + ); + } else if (compose.sourceType === "git") { + command += await getCustomGitCloneCommand( + compose, + deployment.logPath, + true + ); + } else if (compose.sourceType === "raw") { + command += getCreateComposeFileCommand(compose, deployment.logPath); + } + + await execAsyncRemote(compose.serverId, command); + await getBuildComposeCommand(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + + await sendBuildSuccessNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + buildLink, + adminId: compose.project.adminId, + }); + } catch (error) { + // @ts-ignore + const encodedContent = encodeBase64(error?.message); + + await execAsyncRemote( + compose.serverId, + ` echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - await sendBuildErrorNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - // @ts-ignore - errorMessage: error?.message || "Error to build", - buildLink, - adminId: compose.project.adminId, - }); - throw error; - } + echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";` + ); + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + await sendBuildErrorNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + adminId: compose.project.adminId, + }); + throw error; + } }; export const rebuildRemoteCompose = async ({ - composeId, - titleLog = "Rebuild deployment", - descriptionLog = "", + composeId, + titleLog = "Rebuild deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.serverId) { - await getBuildComposeCommand(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - compose.serverId, - ` + const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.serverId) { + await getBuildComposeCommand(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + // @ts-ignore + const encodedContent = encodeBase64(error?.message); + + await execAsyncRemote( + compose.serverId, + ` echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; + echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";` + ); + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; }; export const removeCompose = async ( - compose: Compose, - deleteVolumes: boolean, + compose: Compose, + deleteVolumes: boolean ) => { - try { - const { COMPOSE_PATH } = paths(!!compose.serverId); - const projectPath = join(COMPOSE_PATH, compose.appName); - - console.log("API: DELETE VOLUMES=", deleteVolumes); - - if (compose.composeType === "stack") { - const listVolumesCommand = `docker volume ls --format \"{{.Name}}\" | grep ${compose.appName}`; - const removeVolumesCommand = `${listVolumesCommand} | xargs -r docker volume rm`; - let command: string; - if (deleteVolumes) { - command = `cd ${projectPath} && docker stack rm ${compose.appName} && ${removeVolumesCommand} && rm -rf ${projectPath}`; - } else { - command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; - } - - if (compose.serverId) { - await execAsyncRemote(compose.serverId, command); - } else { - await execAsync(command); - } - await execAsync(command, { - cwd: projectPath, - }); - } else { - let command: string; - if (deleteVolumes) { - command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`; - } else { - command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`; - } - - if (compose.serverId) { - await execAsyncRemote(compose.serverId, command); - } else { - await execAsync(command, { - cwd: projectPath, - }); - } - } - } catch (error) { - throw error; - } - - return true; + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + const projectPath = join(COMPOSE_PATH, compose.appName); + + console.log("API: DELETE VOLUMES=", deleteVolumes); + + if (compose.composeType === "stack") { + const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; + + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } + await execAsync(command, { + cwd: projectPath, + }); + } else { + let command: string; + if (deleteVolumes) { + command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`; + } else { + command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`; + } + + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command, { + cwd: projectPath, + }); + } + } + } catch (error) { + throw error; + } + + return true; }; export const startCompose = async (composeId: string) => { - const compose = await findComposeById(composeId); - try { - const { COMPOSE_PATH } = paths(!!compose.serverId); - if (compose.composeType === "docker-compose") { - if (compose.serverId) { - await execAsyncRemote( - compose.serverId, - `cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`, - ); - } else { - await execAsync(`docker compose -p ${compose.appName} up -d`, { - cwd: join(COMPOSE_PATH, compose.appName, "code"), - }); - } - } - - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - await updateCompose(composeId, { - composeStatus: "idle", - }); - throw error; - } - - return true; + const compose = await findComposeById(composeId); + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + if (compose.composeType === "docker-compose") { + if (compose.serverId) { + await execAsyncRemote( + compose.serverId, + `cd ${join( + COMPOSE_PATH, + compose.appName, + "code" + )} && docker compose -p ${compose.appName} up -d` + ); + } else { + await execAsync(`docker compose -p ${compose.appName} up -d`, { + cwd: join(COMPOSE_PATH, compose.appName, "code"), + }); + } + } + + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + await updateCompose(composeId, { + composeStatus: "idle", + }); + throw error; + } + + return true; }; export const stopCompose = async (composeId: string) => { - const compose = await findComposeById(composeId); - try { - const { COMPOSE_PATH } = paths(!!compose.serverId); - if (compose.composeType === "docker-compose") { - if (compose.serverId) { - await execAsyncRemote( - compose.serverId, - `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`, - ); - } else { - await execAsync(`docker compose -p ${compose.appName} stop`, { - cwd: join(COMPOSE_PATH, compose.appName), - }); - } - } - - await updateCompose(composeId, { - composeStatus: "idle", - }); - } catch (error) { - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; + const compose = await findComposeById(composeId); + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + if (compose.composeType === "docker-compose") { + if (compose.serverId) { + await execAsyncRemote( + compose.serverId, + `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${ + compose.appName + } stop` + ); + } else { + await execAsync(`docker compose -p ${compose.appName} stop`, { + cwd: join(COMPOSE_PATH, compose.appName), + }); + } + } + + await updateCompose(composeId, { + composeStatus: "idle", + }); + } catch (error) { + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; }; From 375decebb29f43042b3a3e8e51e12bcb20dc1a8a Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Mon, 23 Dec 2024 08:36:18 +0100 Subject: [PATCH 48/85] chore: remove application delete volume --- .../application/delete-application.tsx | 291 ++-- .../dokploy/server/api/routers/application.ts | 1217 ++++++++--------- packages/server/src/db/schema/application.ts | 831 ++++++----- 3 files changed, 1152 insertions(+), 1187 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/delete-application.tsx b/apps/dokploy/components/dashboard/application/delete-application.tsx index ff63ef5c5..7202b6f13 100644 --- a/apps/dokploy/components/dashboard/application/delete-application.tsx +++ b/apps/dokploy/components/dashboard/application/delete-application.tsx @@ -1,22 +1,21 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; @@ -29,158 +28,134 @@ import { toast } from "sonner"; import { z } from "zod"; const deleteApplicationSchema = z.object({ - projectName: z.string().min(1, { - message: "Application name is required", - }), - deleteVolumes: z.boolean(), + projectName: z.string().min(1, { + message: "Application name is required", + }), }); type DeleteApplication = z.infer; interface Props { - applicationId: string; + applicationId: string; } export const DeleteApplication = ({ applicationId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.application.delete.useMutation(); - const { data } = api.application.one.useQuery( - { applicationId }, - { enabled: !!applicationId }, - ); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - deleteVolumes: false, - }, - resolver: zodResolver(deleteApplicationSchema), - }); + const [isOpen, setIsOpen] = useState(false); + const { mutateAsync, isLoading } = api.application.delete.useMutation(); + const { data } = api.application.one.useQuery( + { applicationId }, + { enabled: !!applicationId } + ); + const { push } = useRouter(); + const form = useForm({ + defaultValues: { + projectName: "", + }, + resolver: zodResolver(deleteApplicationSchema), + }); - const onSubmit = async (formData: DeleteApplication) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ - applicationId, - deleteVolumes: formData.deleteVolumes, - }) - .then((data) => { - push(`/dashboard/project/${data?.projectId}`); - toast.success("Application deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the application"); - }); - } else { - form.setError("projectName", { - message: "Project name does not match", - }); - } - }; + const onSubmit = async (formData: DeleteApplication) => { + const expectedName = `${data?.name}/${data?.appName}`; + if (formData.projectName === expectedName) { + await mutateAsync({ + applicationId, + }) + .then((data) => { + push(`/dashboard/project/${data?.projectId}`); + toast.success("Application deleted successfully"); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error deleting the application"); + }); + } else { + form.setError("projectName", { + message: "Project name does not match", + }); + } + }; - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - application. If you are sure please enter the application name to - delete this application. - - -
-
- - ( - - - - To confirm, type{" "} - { - if (data?.name && data?.appName) { - navigator.clipboard.writeText( - `${data.name}/${data.appName}`, - ); - toast.success("Copied to clipboard. Be careful!"); - } - }} - > - {data?.name}/{data?.appName}  - - {" "} - in the box below: - - - - - - - - )} - /> - ( - -
- - - - - - Delete volumes associated with this compose - -
- -
- )} - /> - - -
- - - - -
-
- ); + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + application. If you are sure please enter the application name to + delete this application. + + +
+
+ + ( + + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}` + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + + + + + + + + )} + /> + + +
+ + + + +
+
+ ); }; diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 9b16d5796..e2fe4cb4e 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1,55 +1,54 @@ import { - createTRPCRouter, - protectedProcedure, - uploadProcedure, + createTRPCRouter, + protectedProcedure, + uploadProcedure, } from "@/server/api/trpc"; import { db } from "@/server/db"; import { - apiCreateApplication, - apiDeleteApplication, - apiFindMonitoringStats, - apiFindOneApplication, - apiReloadApplication, - apiSaveBitbucketProvider, - apiSaveBuildType, - apiSaveDockerProvider, - apiSaveEnvironmentVariables, - apiSaveGitProvider, - apiSaveGithubProvider, - apiSaveGitlabProvider, - apiUpdateApplication, - applications, + apiCreateApplication, + apiFindMonitoringStats, + apiFindOneApplication, + apiReloadApplication, + apiSaveBitbucketProvider, + apiSaveBuildType, + apiSaveDockerProvider, + apiSaveEnvironmentVariables, + apiSaveGitProvider, + apiSaveGithubProvider, + apiSaveGitlabProvider, + apiUpdateApplication, + applications, } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; import { uploadFileSchema } from "@/utils/schema"; import { - IS_CLOUD, - addNewService, - checkServiceAccess, - createApplication, - deleteAllMiddlewares, - findApplicationById, - findProjectById, - getApplicationStats, - readConfig, - readRemoteConfig, - removeDeployments, - removeDirectoryCode, - removeMonitoringDirectory, - removeService, - removeTraefikConfig, - startService, - startServiceRemote, - stopService, - stopServiceRemote, - unzipDrop, - updateApplication, - updateApplicationStatus, - writeConfig, - writeConfigRemote, - // uploadFileSchema + IS_CLOUD, + addNewService, + checkServiceAccess, + createApplication, + deleteAllMiddlewares, + findApplicationById, + findProjectById, + getApplicationStats, + readConfig, + readRemoteConfig, + removeDeployments, + removeDirectoryCode, + removeMonitoringDirectory, + removeService, + removeTraefikConfig, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + unzipDrop, + updateApplication, + updateApplicationStatus, + writeConfig, + writeConfigRemote, + // uploadFileSchema } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; @@ -57,573 +56,569 @@ import { nanoid } from "nanoid"; import { z } from "zod"; export const applicationRouter = createTRPCRouter({ - create: protectedProcedure - .input(apiCreateApplication) - .mutation(async ({ input, ctx }) => { - try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); - } - - if (IS_CLOUD && !input.serverId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You need to use a server to create an application", - }); - } - - const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this project", - }); - } - const newApplication = await createApplication(input); - - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newApplication.applicationId); - } - return newApplication; - } catch (error: unknown) { - if (error instanceof TRPCError) { - throw error; - } - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error to create the application", - cause: error, - }); - } - }), - one: protectedProcedure - .input(apiFindOneApplication) - .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess( - ctx.user.authId, - input.applicationId, - "access", - ); - } - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return application; - }), - - reload: protectedProcedure - .input(apiReloadApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this application", - }); - } - if (application.serverId) { - await stopServiceRemote(application.serverId, input.appName); - } else { - await stopService(input.appName); - } - await updateApplicationStatus(input.applicationId, "idle"); - - if (application.serverId) { - await startServiceRemote(application.serverId, input.appName); - } else { - await startService(input.appName); - } - await updateApplicationStatus(input.applicationId, "done"); - return true; - }), - - delete: protectedProcedure - .input(apiDeleteApplication) - .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess( - ctx.user.authId, - input.applicationId, - "delete", - ); - } - const application = await findApplicationById(input.applicationId); - - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this application", - }); - } - - const result = await db - .delete(applications) - .where(eq(applications.applicationId, input.applicationId)) - .returning(); - - const cleanupOperations = [ - async () => await deleteAllMiddlewares(application), - async () => await removeDeployments(application), - async () => - await removeDirectoryCode(application.appName, application.serverId), - async () => - await removeMonitoringDirectory( - application.appName, - application.serverId, - ), - async () => - await removeTraefikConfig(application.appName, application.serverId), - async () => - await removeService( - application?.appName, - application.serverId, - input.deleteVolumes, - ), - ]; - - for (const operation of cleanupOperations) { - try { - await operation(); - } catch (error) {} - } - - return result[0]; - }), - - stop: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const service = await findApplicationById(input.applicationId); - if (service.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this application", - }); - } - if (service.serverId) { - await stopServiceRemote(service.serverId, service.appName); - } else { - await stopService(service.appName); - } - await updateApplicationStatus(input.applicationId, "idle"); - - return service; - }), - - start: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const service = await findApplicationById(input.applicationId); - if (service.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this application", - }); - } - - if (service.serverId) { - await startServiceRemote(service.serverId, service.appName); - } else { - await startService(service.appName); - } - await updateApplicationStatus(input.applicationId, "done"); - - return service; - }), - - redeploy: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to redeploy this application", - }); - } - const jobData: DeploymentJob = { - applicationId: input.applicationId, - titleLog: "Rebuild deployment", - descriptionLog: "", - type: "redeploy", - applicationType: "application", - server: !!application.serverId, - }; - - if (IS_CLOUD && application.serverId) { - jobData.serverId = application.serverId; - await deploy(jobData); - return true; - } - await myQueue.add( - "deployments", - { ...jobData }, - { - removeOnComplete: true, - removeOnFail: true, - }, - ); - }), - saveEnvironment: protectedProcedure - .input(apiSaveEnvironmentVariables) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } - await updateApplication(input.applicationId, { - env: input.env, - buildArgs: input.buildArgs, - }); - return true; - }), - saveBuildType: protectedProcedure - .input(apiSaveBuildType) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this build type", - }); - } - await updateApplication(input.applicationId, { - buildType: input.buildType, - dockerfile: input.dockerfile, - publishDirectory: input.publishDirectory, - dockerContextPath: input.dockerContextPath, - dockerBuildStage: input.dockerBuildStage, - herokuVersion: input.herokuVersion, - }); - - return true; - }), - saveGithubProvider: protectedProcedure - .input(apiSaveGithubProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this github provider", - }); - } - await updateApplication(input.applicationId, { - repository: input.repository, - branch: input.branch, - sourceType: "github", - owner: input.owner, - buildPath: input.buildPath, - applicationStatus: "idle", - githubId: input.githubId, - }); - - return true; - }), - saveGitlabProvider: protectedProcedure - .input(apiSaveGitlabProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this gitlab provider", - }); - } - await updateApplication(input.applicationId, { - gitlabRepository: input.gitlabRepository, - gitlabOwner: input.gitlabOwner, - gitlabBranch: input.gitlabBranch, - gitlabBuildPath: input.gitlabBuildPath, - sourceType: "gitlab", - applicationStatus: "idle", - gitlabId: input.gitlabId, - gitlabProjectId: input.gitlabProjectId, - gitlabPathNamespace: input.gitlabPathNamespace, - }); - - return true; - }), - saveBitbucketProvider: protectedProcedure - .input(apiSaveBitbucketProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this bitbucket provider", - }); - } - await updateApplication(input.applicationId, { - bitbucketRepository: input.bitbucketRepository, - bitbucketOwner: input.bitbucketOwner, - bitbucketBranch: input.bitbucketBranch, - bitbucketBuildPath: input.bitbucketBuildPath, - sourceType: "bitbucket", - applicationStatus: "idle", - bitbucketId: input.bitbucketId, - }); - - return true; - }), - saveDockerProvider: protectedProcedure - .input(apiSaveDockerProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this docker provider", - }); - } - await updateApplication(input.applicationId, { - dockerImage: input.dockerImage, - username: input.username, - password: input.password, - sourceType: "docker", - applicationStatus: "idle", - registryUrl: input.registryUrl, - }); - - return true; - }), - saveGitProdiver: protectedProcedure - .input(apiSaveGitProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this git provider", - }); - } - await updateApplication(input.applicationId, { - customGitBranch: input.customGitBranch, - customGitBuildPath: input.customGitBuildPath, - customGitUrl: input.customGitUrl, - customGitSSHKeyId: input.customGitSSHKeyId, - sourceType: "git", - applicationStatus: "idle", - }); - - return true; - }), - markRunning: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to mark this application as running", - }); - } - await updateApplicationStatus(input.applicationId, "running"); - }), - update: protectedProcedure - .input(apiUpdateApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this application", - }); - } - const { applicationId, ...rest } = input; - const updateApp = await updateApplication(applicationId, { - ...rest, - }); - - if (!updateApp) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Update: Error to update application", - }); - } - - return true; - }), - refreshToken: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to refresh this application", - }); - } - await updateApplication(input.applicationId, { - refreshToken: nanoid(), - }); - return true; - }), - deploy: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this application", - }); - } - const jobData: DeploymentJob = { - applicationId: input.applicationId, - titleLog: "Manual deployment", - descriptionLog: "", - type: "deploy", - applicationType: "application", - server: !!application.serverId, - }; - if (IS_CLOUD && application.serverId) { - jobData.serverId = application.serverId; - await deploy(jobData); - - return true; - } - await myQueue.add( - "deployments", - { ...jobData }, - { - removeOnComplete: true, - removeOnFail: true, - }, - ); - }), - - cleanQueues: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to clean this application", - }); - } - await cleanQueuesByApplication(input.applicationId); - }), - - readTraefikConfig: protectedProcedure - .input(apiFindOneApplication) - .query(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to read this application", - }); - } - - let traefikConfig = null; - if (application.serverId) { - traefikConfig = await readRemoteConfig( - application.serverId, - application.appName, - ); - } else { - traefikConfig = readConfig(application.appName); - } - return traefikConfig; - }), - - dropDeployment: protectedProcedure - .meta({ - openapi: { - path: "/drop-deployment", - method: "POST", - override: true, - enabled: false, - }, - }) - .use(uploadProcedure) - .input(uploadFileSchema) - .mutation(async ({ input, ctx }) => { - const zipFile = input.zip; - - const app = await findApplicationById(input.applicationId as string); - - if (app.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this application", - }); - } - - updateApplication(input.applicationId as string, { - sourceType: "drop", - dropBuildPath: input.dropBuildPath, - }); - - await unzipDrop(zipFile, app); - const jobData: DeploymentJob = { - applicationId: app.applicationId, - titleLog: "Manual deployment", - descriptionLog: "", - type: "deploy", - applicationType: "application", - server: !!app.serverId, - }; - if (IS_CLOUD && app.serverId) { - jobData.serverId = app.serverId; - await deploy(jobData); - return true; - } - - await myQueue.add( - "deployments", - { ...jobData }, - { - removeOnComplete: true, - removeOnFail: true, - }, - ); - return true; - }), - updateTraefikConfig: protectedProcedure - .input(z.object({ applicationId: z.string(), traefikConfig: z.string() })) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this application", - }); - } - - if (application.serverId) { - await writeConfigRemote( - application.serverId, - application.appName, - input.traefikConfig, - ); - } else { - writeConfig(application.appName, input.traefikConfig); - } - return true; - }), - readAppMonitoring: protectedProcedure - .input(apiFindMonitoringStats) - .query(async ({ input, ctx }) => { - if (IS_CLOUD) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Functionality not available in cloud version", - }); - } - const stats = await getApplicationStats(input.appName); - - return stats; - }), + create: protectedProcedure + .input(apiCreateApplication) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create an application", + }); + } + + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newApplication = await createApplication(input); + + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newApplication.applicationId); + } + return newApplication; + } catch (error: unknown) { + if (error instanceof TRPCError) { + throw error; + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the application", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneApplication) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess( + ctx.user.authId, + input.applicationId, + "access" + ); + } + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return application; + }), + + reload: protectedProcedure + .input(apiReloadApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this application", + }); + } + if (application.serverId) { + await stopServiceRemote(application.serverId, input.appName); + } else { + await stopService(input.appName); + } + await updateApplicationStatus(input.applicationId, "idle"); + + if (application.serverId) { + await startServiceRemote(application.serverId, input.appName); + } else { + await startService(input.appName); + } + await updateApplicationStatus(input.applicationId, "done"); + return true; + }), + + delete: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess( + ctx.user.authId, + input.applicationId, + "delete" + ); + } + const application = await findApplicationById(input.applicationId); + + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this application", + }); + } + + const result = await db + .delete(applications) + .where(eq(applications.applicationId, input.applicationId)) + .returning(); + + const cleanupOperations = [ + async () => await deleteAllMiddlewares(application), + async () => await removeDeployments(application), + async () => + await removeDirectoryCode(application.appName, application.serverId), + async () => + await removeMonitoringDirectory( + application.appName, + application.serverId + ), + async () => + await removeTraefikConfig(application.appName, application.serverId), + async () => + await removeService(application?.appName, application.serverId), + ]; + + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + + return result[0]; + }), + + stop: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const service = await findApplicationById(input.applicationId); + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to stop this application", + }); + } + if (service.serverId) { + await stopServiceRemote(service.serverId, service.appName); + } else { + await stopService(service.appName); + } + await updateApplicationStatus(input.applicationId, "idle"); + + return service; + }), + + start: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const service = await findApplicationById(input.applicationId); + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to start this application", + }); + } + + if (service.serverId) { + await startServiceRemote(service.serverId, service.appName); + } else { + await startService(service.appName); + } + await updateApplicationStatus(input.applicationId, "done"); + + return service; + }), + + redeploy: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to redeploy this application", + }); + } + const jobData: DeploymentJob = { + applicationId: input.applicationId, + titleLog: "Rebuild deployment", + descriptionLog: "", + type: "redeploy", + applicationType: "application", + server: !!application.serverId, + }; + + if (IS_CLOUD && application.serverId) { + jobData.serverId = application.serverId; + await deploy(jobData); + return true; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + } + ); + }), + saveEnvironment: protectedProcedure + .input(apiSaveEnvironmentVariables) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + await updateApplication(input.applicationId, { + env: input.env, + buildArgs: input.buildArgs, + }); + return true; + }), + saveBuildType: protectedProcedure + .input(apiSaveBuildType) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this build type", + }); + } + await updateApplication(input.applicationId, { + buildType: input.buildType, + dockerfile: input.dockerfile, + publishDirectory: input.publishDirectory, + dockerContextPath: input.dockerContextPath, + dockerBuildStage: input.dockerBuildStage, + herokuVersion: input.herokuVersion, + }); + + return true; + }), + saveGithubProvider: protectedProcedure + .input(apiSaveGithubProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this github provider", + }); + } + await updateApplication(input.applicationId, { + repository: input.repository, + branch: input.branch, + sourceType: "github", + owner: input.owner, + buildPath: input.buildPath, + applicationStatus: "idle", + githubId: input.githubId, + }); + + return true; + }), + saveGitlabProvider: protectedProcedure + .input(apiSaveGitlabProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this gitlab provider", + }); + } + await updateApplication(input.applicationId, { + gitlabRepository: input.gitlabRepository, + gitlabOwner: input.gitlabOwner, + gitlabBranch: input.gitlabBranch, + gitlabBuildPath: input.gitlabBuildPath, + sourceType: "gitlab", + applicationStatus: "idle", + gitlabId: input.gitlabId, + gitlabProjectId: input.gitlabProjectId, + gitlabPathNamespace: input.gitlabPathNamespace, + }); + + return true; + }), + saveBitbucketProvider: protectedProcedure + .input(apiSaveBitbucketProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this bitbucket provider", + }); + } + await updateApplication(input.applicationId, { + bitbucketRepository: input.bitbucketRepository, + bitbucketOwner: input.bitbucketOwner, + bitbucketBranch: input.bitbucketBranch, + bitbucketBuildPath: input.bitbucketBuildPath, + sourceType: "bitbucket", + applicationStatus: "idle", + bitbucketId: input.bitbucketId, + }); + + return true; + }), + saveDockerProvider: protectedProcedure + .input(apiSaveDockerProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this docker provider", + }); + } + await updateApplication(input.applicationId, { + dockerImage: input.dockerImage, + username: input.username, + password: input.password, + sourceType: "docker", + applicationStatus: "idle", + registryUrl: input.registryUrl, + }); + + return true; + }), + saveGitProdiver: protectedProcedure + .input(apiSaveGitProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this git provider", + }); + } + await updateApplication(input.applicationId, { + customGitBranch: input.customGitBranch, + customGitBuildPath: input.customGitBuildPath, + customGitUrl: input.customGitUrl, + customGitSSHKeyId: input.customGitSSHKeyId, + sourceType: "git", + applicationStatus: "idle", + }); + + return true; + }), + markRunning: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to mark this application as running", + }); + } + await updateApplicationStatus(input.applicationId, "running"); + }), + update: protectedProcedure + .input(apiUpdateApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this application", + }); + } + const { applicationId, ...rest } = input; + const updateApp = await updateApplication(applicationId, { + ...rest, + }); + + if (!updateApp) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update application", + }); + } + + return true; + }), + refreshToken: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to refresh this application", + }); + } + await updateApplication(input.applicationId, { + refreshToken: nanoid(), + }); + return true; + }), + deploy: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this application", + }); + } + const jobData: DeploymentJob = { + applicationId: input.applicationId, + titleLog: "Manual deployment", + descriptionLog: "", + type: "deploy", + applicationType: "application", + server: !!application.serverId, + }; + if (IS_CLOUD && application.serverId) { + jobData.serverId = application.serverId; + await deploy(jobData); + + return true; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + } + ); + }), + + cleanQueues: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to clean this application", + }); + } + await cleanQueuesByApplication(input.applicationId); + }), + + readTraefikConfig: protectedProcedure + .input(apiFindOneApplication) + .query(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to read this application", + }); + } + + let traefikConfig = null; + if (application.serverId) { + traefikConfig = await readRemoteConfig( + application.serverId, + application.appName + ); + } else { + traefikConfig = readConfig(application.appName); + } + return traefikConfig; + }), + + dropDeployment: protectedProcedure + .meta({ + openapi: { + path: "/drop-deployment", + method: "POST", + override: true, + enabled: false, + }, + }) + .use(uploadProcedure) + .input(uploadFileSchema) + .mutation(async ({ input, ctx }) => { + const zipFile = input.zip; + + const app = await findApplicationById(input.applicationId as string); + + if (app.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this application", + }); + } + + updateApplication(input.applicationId as string, { + sourceType: "drop", + dropBuildPath: input.dropBuildPath, + }); + + await unzipDrop(zipFile, app); + const jobData: DeploymentJob = { + applicationId: app.applicationId, + titleLog: "Manual deployment", + descriptionLog: "", + type: "deploy", + applicationType: "application", + server: !!app.serverId, + }; + if (IS_CLOUD && app.serverId) { + jobData.serverId = app.serverId; + await deploy(jobData); + return true; + } + + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + } + ); + return true; + }), + updateTraefikConfig: protectedProcedure + .input(z.object({ applicationId: z.string(), traefikConfig: z.string() })) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this application", + }); + } + + if (application.serverId) { + await writeConfigRemote( + application.serverId, + application.appName, + input.traefikConfig + ); + } else { + writeConfig(application.appName, input.traefikConfig); + } + return true; + }), + readAppMonitoring: protectedProcedure + .input(apiFindMonitoringStats) + .query(async ({ input, ctx }) => { + if (IS_CLOUD) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Functionality not available in cloud version", + }); + } + const stats = await getApplicationStats(input.appName); + + return stats; + }), }); diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index 923ea130e..425f8d134 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -1,11 +1,11 @@ import { relations } from "drizzle-orm"; import { - boolean, - integer, - json, - pgEnum, - pgTable, - text, + boolean, + integer, + json, + pgEnum, + pgTable, + text, } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; @@ -28,498 +28,493 @@ import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; export const sourceType = pgEnum("sourceType", [ - "docker", - "git", - "github", - "gitlab", - "bitbucket", - "drop", + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "drop", ]); export const buildType = pgEnum("buildType", [ - "dockerfile", - "heroku_buildpacks", - "paketo_buildpacks", - "nixpacks", - "static", + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", ]); // TODO: refactor this types export interface HealthCheckSwarm { - Test?: string[] | undefined; - Interval?: number | undefined; - Timeout?: number | undefined; - StartPeriod?: number | undefined; - Retries?: number | undefined; + Test?: string[] | undefined; + Interval?: number | undefined; + Timeout?: number | undefined; + StartPeriod?: number | undefined; + Retries?: number | undefined; } export interface RestartPolicySwarm { - Condition?: string | undefined; - Delay?: number | undefined; - MaxAttempts?: number | undefined; - Window?: number | undefined; + Condition?: string | undefined; + Delay?: number | undefined; + MaxAttempts?: number | undefined; + Window?: number | undefined; } export interface PlacementSwarm { - Constraints?: string[] | undefined; - Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined; - MaxReplicas?: number | undefined; - Platforms?: - | Array<{ - Architecture: string; - OS: string; - }> - | undefined; + Constraints?: string[] | undefined; + Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined; + MaxReplicas?: number | undefined; + Platforms?: + | Array<{ + Architecture: string; + OS: string; + }> + | undefined; } export interface UpdateConfigSwarm { - Parallelism: number; - Delay?: number | undefined; - FailureAction?: string | undefined; - Monitor?: number | undefined; - MaxFailureRatio?: number | undefined; - Order: string; + Parallelism: number; + Delay?: number | undefined; + FailureAction?: string | undefined; + Monitor?: number | undefined; + MaxFailureRatio?: number | undefined; + Order: string; } export interface ServiceModeSwarm { - Replicated?: { Replicas?: number | undefined } | undefined; - Global?: {} | undefined; - ReplicatedJob?: - | { - MaxConcurrent?: number | undefined; - TotalCompletions?: number | undefined; - } - | undefined; - GlobalJob?: {} | undefined; + Replicated?: { Replicas?: number | undefined } | undefined; + Global?: {} | undefined; + ReplicatedJob?: + | { + MaxConcurrent?: number | undefined; + TotalCompletions?: number | undefined; + } + | undefined; + GlobalJob?: {} | undefined; } export interface NetworkSwarm { - Target?: string | undefined; - Aliases?: string[] | undefined; - DriverOpts?: { [key: string]: string } | undefined; + Target?: string | undefined; + Aliases?: string[] | undefined; + DriverOpts?: { [key: string]: string } | undefined; } export interface LabelsSwarm { - [name: string]: string; + [name: string]: string; } export const applications = pgTable("application", { - applicationId: text("applicationId") - .notNull() - .primaryKey() - .$defaultFn(() => nanoid()), - name: text("name").notNull(), - appName: text("appName") - .notNull() - .$defaultFn(() => generateAppName("app")) - .unique(), - description: text("description"), - env: text("env"), - previewEnv: text("previewEnv"), - previewBuildArgs: text("previewBuildArgs"), - previewWildcard: text("previewWildcard"), - previewPort: integer("previewPort").default(3000), - previewHttps: boolean("previewHttps").notNull().default(false), - previewPath: text("previewPath").default("/"), - previewCertificateType: certificateType("certificateType") - .notNull() - .default("none"), - previewLimit: integer("previewLimit").default(3), - isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( - false, - ), - buildArgs: text("buildArgs"), - memoryReservation: integer("memoryReservation"), - memoryLimit: integer("memoryLimit"), - cpuReservation: integer("cpuReservation"), - cpuLimit: integer("cpuLimit"), - title: text("title"), - enabled: boolean("enabled"), - subtitle: text("subtitle"), - command: text("command"), - refreshToken: text("refreshToken").$defaultFn(() => nanoid()), - sourceType: sourceType("sourceType").notNull().default("github"), - // Github - repository: text("repository"), - owner: text("owner"), - branch: text("branch"), - buildPath: text("buildPath").default("/"), - autoDeploy: boolean("autoDeploy").$defaultFn(() => true), - // Gitlab - gitlabProjectId: integer("gitlabProjectId"), - gitlabRepository: text("gitlabRepository"), - gitlabOwner: text("gitlabOwner"), - gitlabBranch: text("gitlabBranch"), - gitlabBuildPath: text("gitlabBuildPath").default("/"), - gitlabPathNamespace: text("gitlabPathNamespace"), - // Bitbucket - bitbucketRepository: text("bitbucketRepository"), - bitbucketOwner: text("bitbucketOwner"), - bitbucketBranch: text("bitbucketBranch"), - bitbucketBuildPath: text("bitbucketBuildPath").default("/"), - // Docker - username: text("username"), - password: text("password"), - dockerImage: text("dockerImage"), - registryUrl: text("registryUrl"), - // Git - customGitUrl: text("customGitUrl"), - customGitBranch: text("customGitBranch"), - customGitBuildPath: text("customGitBuildPath"), - customGitSSHKeyId: text("customGitSSHKeyId").references( - () => sshKeys.sshKeyId, - { - onDelete: "set null", - }, - ), - dockerfile: text("dockerfile"), - dockerContextPath: text("dockerContextPath"), - dockerBuildStage: text("dockerBuildStage"), - // Drop - dropBuildPath: text("dropBuildPath"), - // Docker swarm json - healthCheckSwarm: json("healthCheckSwarm").$type(), - restartPolicySwarm: json("restartPolicySwarm").$type(), - placementSwarm: json("placementSwarm").$type(), - updateConfigSwarm: json("updateConfigSwarm").$type(), - rollbackConfigSwarm: json("rollbackConfigSwarm").$type(), - modeSwarm: json("modeSwarm").$type(), - labelsSwarm: json("labelsSwarm").$type(), - networkSwarm: json("networkSwarm").$type(), - // - replicas: integer("replicas").default(1).notNull(), - applicationStatus: applicationStatus("applicationStatus") - .notNull() - .default("idle"), - buildType: buildType("buildType").notNull().default("nixpacks"), - herokuVersion: text("herokuVersion").default("24"), - publishDirectory: text("publishDirectory"), - createdAt: text("createdAt") - .notNull() - .$defaultFn(() => new Date().toISOString()), - registryId: text("registryId").references(() => registry.registryId, { - onDelete: "set null", - }), - projectId: text("projectId") - .notNull() - .references(() => projects.projectId, { onDelete: "cascade" }), - githubId: text("githubId").references(() => github.githubId, { - onDelete: "set null", - }), - gitlabId: text("gitlabId").references(() => gitlab.gitlabId, { - onDelete: "set null", - }), - bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, { - onDelete: "set null", - }), - serverId: text("serverId").references(() => server.serverId, { - onDelete: "cascade", - }), + applicationId: text("applicationId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + appName: text("appName") + .notNull() + .$defaultFn(() => generateAppName("app")) + .unique(), + description: text("description"), + env: text("env"), + previewEnv: text("previewEnv"), + previewBuildArgs: text("previewBuildArgs"), + previewWildcard: text("previewWildcard"), + previewPort: integer("previewPort").default(3000), + previewHttps: boolean("previewHttps").notNull().default(false), + previewPath: text("previewPath").default("/"), + previewCertificateType: certificateType("certificateType") + .notNull() + .default("none"), + previewLimit: integer("previewLimit").default(3), + isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( + false + ), + buildArgs: text("buildArgs"), + memoryReservation: integer("memoryReservation"), + memoryLimit: integer("memoryLimit"), + cpuReservation: integer("cpuReservation"), + cpuLimit: integer("cpuLimit"), + title: text("title"), + enabled: boolean("enabled"), + subtitle: text("subtitle"), + command: text("command"), + refreshToken: text("refreshToken").$defaultFn(() => nanoid()), + sourceType: sourceType("sourceType").notNull().default("github"), + // Github + repository: text("repository"), + owner: text("owner"), + branch: text("branch"), + buildPath: text("buildPath").default("/"), + autoDeploy: boolean("autoDeploy").$defaultFn(() => true), + // Gitlab + gitlabProjectId: integer("gitlabProjectId"), + gitlabRepository: text("gitlabRepository"), + gitlabOwner: text("gitlabOwner"), + gitlabBranch: text("gitlabBranch"), + gitlabBuildPath: text("gitlabBuildPath").default("/"), + gitlabPathNamespace: text("gitlabPathNamespace"), + // Bitbucket + bitbucketRepository: text("bitbucketRepository"), + bitbucketOwner: text("bitbucketOwner"), + bitbucketBranch: text("bitbucketBranch"), + bitbucketBuildPath: text("bitbucketBuildPath").default("/"), + // Docker + username: text("username"), + password: text("password"), + dockerImage: text("dockerImage"), + registryUrl: text("registryUrl"), + // Git + customGitUrl: text("customGitUrl"), + customGitBranch: text("customGitBranch"), + customGitBuildPath: text("customGitBuildPath"), + customGitSSHKeyId: text("customGitSSHKeyId").references( + () => sshKeys.sshKeyId, + { + onDelete: "set null", + } + ), + dockerfile: text("dockerfile"), + dockerContextPath: text("dockerContextPath"), + dockerBuildStage: text("dockerBuildStage"), + // Drop + dropBuildPath: text("dropBuildPath"), + // Docker swarm json + healthCheckSwarm: json("healthCheckSwarm").$type(), + restartPolicySwarm: json("restartPolicySwarm").$type(), + placementSwarm: json("placementSwarm").$type(), + updateConfigSwarm: json("updateConfigSwarm").$type(), + rollbackConfigSwarm: json("rollbackConfigSwarm").$type(), + modeSwarm: json("modeSwarm").$type(), + labelsSwarm: json("labelsSwarm").$type(), + networkSwarm: json("networkSwarm").$type(), + // + replicas: integer("replicas").default(1).notNull(), + applicationStatus: applicationStatus("applicationStatus") + .notNull() + .default("idle"), + buildType: buildType("buildType").notNull().default("nixpacks"), + herokuVersion: text("herokuVersion").default("24"), + publishDirectory: text("publishDirectory"), + createdAt: text("createdAt") + .notNull() + .$defaultFn(() => new Date().toISOString()), + registryId: text("registryId").references(() => registry.registryId, { + onDelete: "set null", + }), + projectId: text("projectId") + .notNull() + .references(() => projects.projectId, { onDelete: "cascade" }), + githubId: text("githubId").references(() => github.githubId, { + onDelete: "set null", + }), + gitlabId: text("gitlabId").references(() => gitlab.gitlabId, { + onDelete: "set null", + }), + bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, { + onDelete: "set null", + }), + serverId: text("serverId").references(() => server.serverId, { + onDelete: "cascade", + }), }); export const applicationsRelations = relations( - applications, - ({ one, many }) => ({ - project: one(projects, { - fields: [applications.projectId], - references: [projects.projectId], - }), - deployments: many(deployments), - customGitSSHKey: one(sshKeys, { - fields: [applications.customGitSSHKeyId], - references: [sshKeys.sshKeyId], - }), - domains: many(domains), - mounts: many(mounts), - redirects: many(redirects), - security: many(security), - ports: many(ports), - registry: one(registry, { - fields: [applications.registryId], - references: [registry.registryId], - }), - github: one(github, { - fields: [applications.githubId], - references: [github.githubId], - }), - gitlab: one(gitlab, { - fields: [applications.gitlabId], - references: [gitlab.gitlabId], - }), - bitbucket: one(bitbucket, { - fields: [applications.bitbucketId], - references: [bitbucket.bitbucketId], - }), - server: one(server, { - fields: [applications.serverId], - references: [server.serverId], - }), - previewDeployments: many(previewDeployments), - }), + applications, + ({ one, many }) => ({ + project: one(projects, { + fields: [applications.projectId], + references: [projects.projectId], + }), + deployments: many(deployments), + customGitSSHKey: one(sshKeys, { + fields: [applications.customGitSSHKeyId], + references: [sshKeys.sshKeyId], + }), + domains: many(domains), + mounts: many(mounts), + redirects: many(redirects), + security: many(security), + ports: many(ports), + registry: one(registry, { + fields: [applications.registryId], + references: [registry.registryId], + }), + github: one(github, { + fields: [applications.githubId], + references: [github.githubId], + }), + gitlab: one(gitlab, { + fields: [applications.gitlabId], + references: [gitlab.gitlabId], + }), + bitbucket: one(bitbucket, { + fields: [applications.bitbucketId], + references: [bitbucket.bitbucketId], + }), + server: one(server, { + fields: [applications.serverId], + references: [server.serverId], + }), + previewDeployments: many(previewDeployments), + }) ); const HealthCheckSwarmSchema = z - .object({ - Test: z.array(z.string()).optional(), - Interval: z.number().optional(), - Timeout: z.number().optional(), - StartPeriod: z.number().optional(), - Retries: z.number().optional(), - }) - .strict(); + .object({ + Test: z.array(z.string()).optional(), + Interval: z.number().optional(), + Timeout: z.number().optional(), + StartPeriod: z.number().optional(), + Retries: z.number().optional(), + }) + .strict(); const RestartPolicySwarmSchema = z - .object({ - Condition: z.string().optional(), - Delay: z.number().optional(), - MaxAttempts: z.number().optional(), - Window: z.number().optional(), - }) - .strict(); + .object({ + Condition: z.string().optional(), + Delay: z.number().optional(), + MaxAttempts: z.number().optional(), + Window: z.number().optional(), + }) + .strict(); const PreferenceSchema = z - .object({ - Spread: z.object({ - SpreadDescriptor: z.string(), - }), - }) - .strict(); + .object({ + Spread: z.object({ + SpreadDescriptor: z.string(), + }), + }) + .strict(); const PlatformSchema = z - .object({ - Architecture: z.string(), - OS: z.string(), - }) - .strict(); + .object({ + Architecture: z.string(), + OS: z.string(), + }) + .strict(); const PlacementSwarmSchema = z - .object({ - Constraints: z.array(z.string()).optional(), - Preferences: z.array(PreferenceSchema).optional(), - MaxReplicas: z.number().optional(), - Platforms: z.array(PlatformSchema).optional(), - }) - .strict(); + .object({ + Constraints: z.array(z.string()).optional(), + Preferences: z.array(PreferenceSchema).optional(), + MaxReplicas: z.number().optional(), + Platforms: z.array(PlatformSchema).optional(), + }) + .strict(); const UpdateConfigSwarmSchema = z - .object({ - Parallelism: z.number(), - Delay: z.number().optional(), - FailureAction: z.string().optional(), - Monitor: z.number().optional(), - MaxFailureRatio: z.number().optional(), - Order: z.string(), - }) - .strict(); + .object({ + Parallelism: z.number(), + Delay: z.number().optional(), + FailureAction: z.string().optional(), + Monitor: z.number().optional(), + MaxFailureRatio: z.number().optional(), + Order: z.string(), + }) + .strict(); const ReplicatedSchema = z - .object({ - Replicas: z.number().optional(), - }) - .strict(); + .object({ + Replicas: z.number().optional(), + }) + .strict(); const ReplicatedJobSchema = z - .object({ - MaxConcurrent: z.number().optional(), - TotalCompletions: z.number().optional(), - }) - .strict(); + .object({ + MaxConcurrent: z.number().optional(), + TotalCompletions: z.number().optional(), + }) + .strict(); const ServiceModeSwarmSchema = z - .object({ - Replicated: ReplicatedSchema.optional(), - Global: z.object({}).optional(), - ReplicatedJob: ReplicatedJobSchema.optional(), - GlobalJob: z.object({}).optional(), - }) - .strict(); + .object({ + Replicated: ReplicatedSchema.optional(), + Global: z.object({}).optional(), + ReplicatedJob: ReplicatedJobSchema.optional(), + GlobalJob: z.object({}).optional(), + }) + .strict(); const NetworkSwarmSchema = z.array( - z - .object({ - Target: z.string().optional(), - Aliases: z.array(z.string()).optional(), - DriverOpts: z.object({}).optional(), - }) - .strict(), + z + .object({ + Target: z.string().optional(), + Aliases: z.array(z.string()).optional(), + DriverOpts: z.object({}).optional(), + }) + .strict() ); const LabelsSwarmSchema = z.record(z.string()); const createSchema = createInsertSchema(applications, { - appName: z.string(), - createdAt: z.string(), - applicationId: z.string(), - autoDeploy: z.boolean(), - env: z.string().optional(), - buildArgs: z.string().optional(), - name: z.string().min(1), - description: z.string().optional(), - memoryReservation: z.number().optional(), - memoryLimit: z.number().optional(), - cpuReservation: z.number().optional(), - cpuLimit: z.number().optional(), - title: z.string().optional(), - enabled: z.boolean().optional(), - subtitle: z.string().optional(), - dockerImage: z.string().optional(), - username: z.string().optional(), - isPreviewDeploymentsActive: z.boolean().optional(), - password: z.string().optional(), - registryUrl: z.string().optional(), - customGitSSHKeyId: z.string().optional(), - repository: z.string().optional(), - dockerfile: z.string().optional(), - branch: z.string().optional(), - customGitBranch: z.string().optional(), - customGitBuildPath: z.string().optional(), - customGitUrl: z.string().optional(), - buildPath: z.string().optional(), - projectId: z.string(), - sourceType: z.enum(["github", "docker", "git"]).optional(), - applicationStatus: z.enum(["idle", "running", "done", "error"]), - buildType: z.enum([ - "dockerfile", - "heroku_buildpacks", - "paketo_buildpacks", - "nixpacks", - "static", - ]), - herokuVersion: z.string().optional(), - publishDirectory: z.string().optional(), - owner: z.string(), - healthCheckSwarm: HealthCheckSwarmSchema.nullable(), - restartPolicySwarm: RestartPolicySwarmSchema.nullable(), - placementSwarm: PlacementSwarmSchema.nullable(), - updateConfigSwarm: UpdateConfigSwarmSchema.nullable(), - rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(), - modeSwarm: ServiceModeSwarmSchema.nullable(), - labelsSwarm: LabelsSwarmSchema.nullable(), - networkSwarm: NetworkSwarmSchema.nullable(), - previewPort: z.number().optional(), - previewEnv: z.string().optional(), - previewBuildArgs: z.string().optional(), - previewWildcard: z.string().optional(), - previewLimit: z.number().optional(), - previewHttps: z.boolean().optional(), - previewPath: z.string().optional(), - previewCertificateType: z.enum(["letsencrypt", "none"]).optional(), + appName: z.string(), + createdAt: z.string(), + applicationId: z.string(), + autoDeploy: z.boolean(), + env: z.string().optional(), + buildArgs: z.string().optional(), + name: z.string().min(1), + description: z.string().optional(), + memoryReservation: z.number().optional(), + memoryLimit: z.number().optional(), + cpuReservation: z.number().optional(), + cpuLimit: z.number().optional(), + title: z.string().optional(), + enabled: z.boolean().optional(), + subtitle: z.string().optional(), + dockerImage: z.string().optional(), + username: z.string().optional(), + isPreviewDeploymentsActive: z.boolean().optional(), + password: z.string().optional(), + registryUrl: z.string().optional(), + customGitSSHKeyId: z.string().optional(), + repository: z.string().optional(), + dockerfile: z.string().optional(), + branch: z.string().optional(), + customGitBranch: z.string().optional(), + customGitBuildPath: z.string().optional(), + customGitUrl: z.string().optional(), + buildPath: z.string().optional(), + projectId: z.string(), + sourceType: z.enum(["github", "docker", "git"]).optional(), + applicationStatus: z.enum(["idle", "running", "done", "error"]), + buildType: z.enum([ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + ]), + herokuVersion: z.string().optional(), + publishDirectory: z.string().optional(), + owner: z.string(), + healthCheckSwarm: HealthCheckSwarmSchema.nullable(), + restartPolicySwarm: RestartPolicySwarmSchema.nullable(), + placementSwarm: PlacementSwarmSchema.nullable(), + updateConfigSwarm: UpdateConfigSwarmSchema.nullable(), + rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(), + modeSwarm: ServiceModeSwarmSchema.nullable(), + labelsSwarm: LabelsSwarmSchema.nullable(), + networkSwarm: NetworkSwarmSchema.nullable(), + previewPort: z.number().optional(), + previewEnv: z.string().optional(), + previewBuildArgs: z.string().optional(), + previewWildcard: z.string().optional(), + previewLimit: z.number().optional(), + previewHttps: z.boolean().optional(), + previewPath: z.string().optional(), + previewCertificateType: z.enum(["letsencrypt", "none"]).optional(), }); export const apiCreateApplication = createSchema.pick({ - name: true, - appName: true, - description: true, - projectId: true, - serverId: true, + name: true, + appName: true, + description: true, + projectId: true, + serverId: true, }); export const apiFindOneApplication = createSchema - .pick({ - applicationId: true, - }) - .required(); + .pick({ + applicationId: true, + }) + .required(); export const apiReloadApplication = createSchema - .pick({ - appName: true, - applicationId: true, - }) - .required(); + .pick({ + appName: true, + applicationId: true, + }) + .required(); export const apiSaveBuildType = createSchema - .pick({ - applicationId: true, - buildType: true, - dockerfile: true, - dockerContextPath: true, - dockerBuildStage: true, - herokuVersion: true, - }) - .required() - .merge(createSchema.pick({ publishDirectory: true })); + .pick({ + applicationId: true, + buildType: true, + dockerfile: true, + dockerContextPath: true, + dockerBuildStage: true, + herokuVersion: true, + }) + .required() + .merge(createSchema.pick({ publishDirectory: true })); export const apiSaveGithubProvider = createSchema - .pick({ - applicationId: true, - repository: true, - branch: true, - owner: true, - buildPath: true, - githubId: true, - }) - .required(); + .pick({ + applicationId: true, + repository: true, + branch: true, + owner: true, + buildPath: true, + githubId: true, + }) + .required(); export const apiSaveGitlabProvider = createSchema - .pick({ - applicationId: true, - gitlabBranch: true, - gitlabBuildPath: true, - gitlabOwner: true, - gitlabRepository: true, - gitlabId: true, - gitlabProjectId: true, - gitlabPathNamespace: true, - }) - .required(); + .pick({ + applicationId: true, + gitlabBranch: true, + gitlabBuildPath: true, + gitlabOwner: true, + gitlabRepository: true, + gitlabId: true, + gitlabProjectId: true, + gitlabPathNamespace: true, + }) + .required(); export const apiSaveBitbucketProvider = createSchema - .pick({ - bitbucketBranch: true, - bitbucketBuildPath: true, - bitbucketOwner: true, - bitbucketRepository: true, - bitbucketId: true, - applicationId: true, - }) - .required(); + .pick({ + bitbucketBranch: true, + bitbucketBuildPath: true, + bitbucketOwner: true, + bitbucketRepository: true, + bitbucketId: true, + applicationId: true, + }) + .required(); export const apiSaveDockerProvider = createSchema - .pick({ - dockerImage: true, - applicationId: true, - username: true, - password: true, - registryUrl: true, - }) - .required(); + .pick({ + dockerImage: true, + applicationId: true, + username: true, + password: true, + registryUrl: true, + }) + .required(); export const apiSaveGitProvider = createSchema - .pick({ - customGitBranch: true, - applicationId: true, - customGitBuildPath: true, - customGitUrl: true, - }) - .required() - .merge( - createSchema.pick({ - customGitSSHKeyId: true, - }), - ); + .pick({ + customGitBranch: true, + applicationId: true, + customGitBuildPath: true, + customGitUrl: true, + }) + .required() + .merge( + createSchema.pick({ + customGitSSHKeyId: true, + }) + ); export const apiSaveEnvironmentVariables = createSchema - .pick({ - applicationId: true, - env: true, - buildArgs: true, - }) - .required(); + .pick({ + applicationId: true, + env: true, + buildArgs: true, + }) + .required(); export const apiFindMonitoringStats = createSchema - .pick({ - appName: true, - }) - .required(); + .pick({ + appName: true, + }) + .required(); export const apiUpdateApplication = createSchema - .partial() - .extend({ - applicationId: z.string().min(1), - }) - .omit({ serverId: true }); - -export const apiDeleteApplication = z.object({ - applicationId: z.string().min(1), - deleteVolumes: z.boolean(), -}); + .partial() + .extend({ + applicationId: z.string().min(1), + }) + .omit({ serverId: true }); From fa710d48556826ccf8d6808a5e1af7b5416d10ac Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Mon, 23 Dec 2024 08:39:11 +0100 Subject: [PATCH 49/85] format: fix formatting --- .../application/delete-application.tsx | 266 ++-- .../dokploy/server/api/routers/application.ts | 1212 ++++++++--------- 2 files changed, 739 insertions(+), 739 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/delete-application.tsx b/apps/dokploy/components/dashboard/application/delete-application.tsx index 7202b6f13..f34d29a78 100644 --- a/apps/dokploy/components/dashboard/application/delete-application.tsx +++ b/apps/dokploy/components/dashboard/application/delete-application.tsx @@ -1,21 +1,21 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; @@ -28,134 +28,134 @@ import { toast } from "sonner"; import { z } from "zod"; const deleteApplicationSchema = z.object({ - projectName: z.string().min(1, { - message: "Application name is required", - }), + projectName: z.string().min(1, { + message: "Application name is required", + }), }); type DeleteApplication = z.infer; interface Props { - applicationId: string; + applicationId: string; } export const DeleteApplication = ({ applicationId }: Props) => { - const [isOpen, setIsOpen] = useState(false); - const { mutateAsync, isLoading } = api.application.delete.useMutation(); - const { data } = api.application.one.useQuery( - { applicationId }, - { enabled: !!applicationId } - ); - const { push } = useRouter(); - const form = useForm({ - defaultValues: { - projectName: "", - }, - resolver: zodResolver(deleteApplicationSchema), - }); + const [isOpen, setIsOpen] = useState(false); + const { mutateAsync, isLoading } = api.application.delete.useMutation(); + const { data } = api.application.one.useQuery( + { applicationId }, + { enabled: !!applicationId }, + ); + const { push } = useRouter(); + const form = useForm({ + defaultValues: { + projectName: "", + }, + resolver: zodResolver(deleteApplicationSchema), + }); - const onSubmit = async (formData: DeleteApplication) => { - const expectedName = `${data?.name}/${data?.appName}`; - if (formData.projectName === expectedName) { - await mutateAsync({ - applicationId, - }) - .then((data) => { - push(`/dashboard/project/${data?.projectId}`); - toast.success("Application deleted successfully"); - setIsOpen(false); - }) - .catch(() => { - toast.error("Error deleting the application"); - }); - } else { - form.setError("projectName", { - message: "Project name does not match", - }); - } - }; + const onSubmit = async (formData: DeleteApplication) => { + const expectedName = `${data?.name}/${data?.appName}`; + if (formData.projectName === expectedName) { + await mutateAsync({ + applicationId, + }) + .then((data) => { + push(`/dashboard/project/${data?.projectId}`); + toast.success("Application deleted successfully"); + setIsOpen(false); + }) + .catch(() => { + toast.error("Error deleting the application"); + }); + } else { + form.setError("projectName", { + message: "Project name does not match", + }); + } + }; - return ( - - - - - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete the - application. If you are sure please enter the application name to - delete this application. - - -
-
- - ( - - - - To confirm, type{" "} - { - if (data?.name && data?.appName) { - navigator.clipboard.writeText( - `${data.name}/${data.appName}` - ); - toast.success("Copied to clipboard. Be careful!"); - } - }} - > - {data?.name}/{data?.appName}  - - {" "} - in the box below: - - - - - - - - )} - /> - - -
- - - - -
-
- ); + return ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete the + application. If you are sure please enter the application name to + delete this application. + + +
+
+ + ( + + + + To confirm, type{" "} + { + if (data?.name && data?.appName) { + navigator.clipboard.writeText( + `${data.name}/${data.appName}`, + ); + toast.success("Copied to clipboard. Be careful!"); + } + }} + > + {data?.name}/{data?.appName}  + + {" "} + in the box below: + + + + + + + + )} + /> + + +
+ + + + +
+
+ ); }; diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index e2fe4cb4e..2902c8eda 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1,54 +1,54 @@ import { - createTRPCRouter, - protectedProcedure, - uploadProcedure, + createTRPCRouter, + protectedProcedure, + uploadProcedure, } from "@/server/api/trpc"; import { db } from "@/server/db"; import { - apiCreateApplication, - apiFindMonitoringStats, - apiFindOneApplication, - apiReloadApplication, - apiSaveBitbucketProvider, - apiSaveBuildType, - apiSaveDockerProvider, - apiSaveEnvironmentVariables, - apiSaveGitProvider, - apiSaveGithubProvider, - apiSaveGitlabProvider, - apiUpdateApplication, - applications, + apiCreateApplication, + apiFindMonitoringStats, + apiFindOneApplication, + apiReloadApplication, + apiSaveBitbucketProvider, + apiSaveBuildType, + apiSaveDockerProvider, + apiSaveEnvironmentVariables, + apiSaveGitProvider, + apiSaveGithubProvider, + apiSaveGitlabProvider, + apiUpdateApplication, + applications, } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; import { uploadFileSchema } from "@/utils/schema"; import { - IS_CLOUD, - addNewService, - checkServiceAccess, - createApplication, - deleteAllMiddlewares, - findApplicationById, - findProjectById, - getApplicationStats, - readConfig, - readRemoteConfig, - removeDeployments, - removeDirectoryCode, - removeMonitoringDirectory, - removeService, - removeTraefikConfig, - startService, - startServiceRemote, - stopService, - stopServiceRemote, - unzipDrop, - updateApplication, - updateApplicationStatus, - writeConfig, - writeConfigRemote, - // uploadFileSchema + IS_CLOUD, + addNewService, + checkServiceAccess, + createApplication, + deleteAllMiddlewares, + findApplicationById, + findProjectById, + getApplicationStats, + readConfig, + readRemoteConfig, + removeDeployments, + removeDirectoryCode, + removeMonitoringDirectory, + removeService, + removeTraefikConfig, + startService, + startServiceRemote, + stopService, + stopServiceRemote, + unzipDrop, + updateApplication, + updateApplicationStatus, + writeConfig, + writeConfigRemote, + // uploadFileSchema } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; @@ -56,569 +56,569 @@ import { nanoid } from "nanoid"; import { z } from "zod"; export const applicationRouter = createTRPCRouter({ - create: protectedProcedure - .input(apiCreateApplication) - .mutation(async ({ input, ctx }) => { - try { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.authId, input.projectId, "create"); - } - - if (IS_CLOUD && !input.serverId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You need to use a server to create an application", - }); - } - - const project = await findProjectById(input.projectId); - if (project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this project", - }); - } - const newApplication = await createApplication(input); - - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, newApplication.applicationId); - } - return newApplication; - } catch (error: unknown) { - if (error instanceof TRPCError) { - throw error; - } - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error to create the application", - cause: error, - }); - } - }), - one: protectedProcedure - .input(apiFindOneApplication) - .query(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess( - ctx.user.authId, - input.applicationId, - "access" - ); - } - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to access this application", - }); - } - return application; - }), - - reload: protectedProcedure - .input(apiReloadApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this application", - }); - } - if (application.serverId) { - await stopServiceRemote(application.serverId, input.appName); - } else { - await stopService(input.appName); - } - await updateApplicationStatus(input.applicationId, "idle"); - - if (application.serverId) { - await startServiceRemote(application.serverId, input.appName); - } else { - await startService(input.appName); - } - await updateApplicationStatus(input.applicationId, "done"); - return true; - }), - - delete: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess( - ctx.user.authId, - input.applicationId, - "delete" - ); - } - const application = await findApplicationById(input.applicationId); - - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to delete this application", - }); - } - - const result = await db - .delete(applications) - .where(eq(applications.applicationId, input.applicationId)) - .returning(); - - const cleanupOperations = [ - async () => await deleteAllMiddlewares(application), - async () => await removeDeployments(application), - async () => - await removeDirectoryCode(application.appName, application.serverId), - async () => - await removeMonitoringDirectory( - application.appName, - application.serverId - ), - async () => - await removeTraefikConfig(application.appName, application.serverId), - async () => - await removeService(application?.appName, application.serverId), - ]; - - for (const operation of cleanupOperations) { - try { - await operation(); - } catch (error) {} - } - - return result[0]; - }), - - stop: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const service = await findApplicationById(input.applicationId); - if (service.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to stop this application", - }); - } - if (service.serverId) { - await stopServiceRemote(service.serverId, service.appName); - } else { - await stopService(service.appName); - } - await updateApplicationStatus(input.applicationId, "idle"); - - return service; - }), - - start: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const service = await findApplicationById(input.applicationId); - if (service.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to start this application", - }); - } - - if (service.serverId) { - await startServiceRemote(service.serverId, service.appName); - } else { - await startService(service.appName); - } - await updateApplicationStatus(input.applicationId, "done"); - - return service; - }), - - redeploy: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to redeploy this application", - }); - } - const jobData: DeploymentJob = { - applicationId: input.applicationId, - titleLog: "Rebuild deployment", - descriptionLog: "", - type: "redeploy", - applicationType: "application", - server: !!application.serverId, - }; - - if (IS_CLOUD && application.serverId) { - jobData.serverId = application.serverId; - await deploy(jobData); - return true; - } - await myQueue.add( - "deployments", - { ...jobData }, - { - removeOnComplete: true, - removeOnFail: true, - } - ); - }), - saveEnvironment: protectedProcedure - .input(apiSaveEnvironmentVariables) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this environment", - }); - } - await updateApplication(input.applicationId, { - env: input.env, - buildArgs: input.buildArgs, - }); - return true; - }), - saveBuildType: protectedProcedure - .input(apiSaveBuildType) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this build type", - }); - } - await updateApplication(input.applicationId, { - buildType: input.buildType, - dockerfile: input.dockerfile, - publishDirectory: input.publishDirectory, - dockerContextPath: input.dockerContextPath, - dockerBuildStage: input.dockerBuildStage, - herokuVersion: input.herokuVersion, - }); - - return true; - }), - saveGithubProvider: protectedProcedure - .input(apiSaveGithubProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this github provider", - }); - } - await updateApplication(input.applicationId, { - repository: input.repository, - branch: input.branch, - sourceType: "github", - owner: input.owner, - buildPath: input.buildPath, - applicationStatus: "idle", - githubId: input.githubId, - }); - - return true; - }), - saveGitlabProvider: protectedProcedure - .input(apiSaveGitlabProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this gitlab provider", - }); - } - await updateApplication(input.applicationId, { - gitlabRepository: input.gitlabRepository, - gitlabOwner: input.gitlabOwner, - gitlabBranch: input.gitlabBranch, - gitlabBuildPath: input.gitlabBuildPath, - sourceType: "gitlab", - applicationStatus: "idle", - gitlabId: input.gitlabId, - gitlabProjectId: input.gitlabProjectId, - gitlabPathNamespace: input.gitlabPathNamespace, - }); - - return true; - }), - saveBitbucketProvider: protectedProcedure - .input(apiSaveBitbucketProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this bitbucket provider", - }); - } - await updateApplication(input.applicationId, { - bitbucketRepository: input.bitbucketRepository, - bitbucketOwner: input.bitbucketOwner, - bitbucketBranch: input.bitbucketBranch, - bitbucketBuildPath: input.bitbucketBuildPath, - sourceType: "bitbucket", - applicationStatus: "idle", - bitbucketId: input.bitbucketId, - }); - - return true; - }), - saveDockerProvider: protectedProcedure - .input(apiSaveDockerProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this docker provider", - }); - } - await updateApplication(input.applicationId, { - dockerImage: input.dockerImage, - username: input.username, - password: input.password, - sourceType: "docker", - applicationStatus: "idle", - registryUrl: input.registryUrl, - }); - - return true; - }), - saveGitProdiver: protectedProcedure - .input(apiSaveGitProvider) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to save this git provider", - }); - } - await updateApplication(input.applicationId, { - customGitBranch: input.customGitBranch, - customGitBuildPath: input.customGitBuildPath, - customGitUrl: input.customGitUrl, - customGitSSHKeyId: input.customGitSSHKeyId, - sourceType: "git", - applicationStatus: "idle", - }); - - return true; - }), - markRunning: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to mark this application as running", - }); - } - await updateApplicationStatus(input.applicationId, "running"); - }), - update: protectedProcedure - .input(apiUpdateApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this application", - }); - } - const { applicationId, ...rest } = input; - const updateApp = await updateApplication(applicationId, { - ...rest, - }); - - if (!updateApp) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Update: Error to update application", - }); - } - - return true; - }), - refreshToken: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to refresh this application", - }); - } - await updateApplication(input.applicationId, { - refreshToken: nanoid(), - }); - return true; - }), - deploy: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this application", - }); - } - const jobData: DeploymentJob = { - applicationId: input.applicationId, - titleLog: "Manual deployment", - descriptionLog: "", - type: "deploy", - applicationType: "application", - server: !!application.serverId, - }; - if (IS_CLOUD && application.serverId) { - jobData.serverId = application.serverId; - await deploy(jobData); - - return true; - } - await myQueue.add( - "deployments", - { ...jobData }, - { - removeOnComplete: true, - removeOnFail: true, - } - ); - }), - - cleanQueues: protectedProcedure - .input(apiFindOneApplication) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to clean this application", - }); - } - await cleanQueuesByApplication(input.applicationId); - }), - - readTraefikConfig: protectedProcedure - .input(apiFindOneApplication) - .query(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to read this application", - }); - } - - let traefikConfig = null; - if (application.serverId) { - traefikConfig = await readRemoteConfig( - application.serverId, - application.appName - ); - } else { - traefikConfig = readConfig(application.appName); - } - return traefikConfig; - }), - - dropDeployment: protectedProcedure - .meta({ - openapi: { - path: "/drop-deployment", - method: "POST", - override: true, - enabled: false, - }, - }) - .use(uploadProcedure) - .input(uploadFileSchema) - .mutation(async ({ input, ctx }) => { - const zipFile = input.zip; - - const app = await findApplicationById(input.applicationId as string); - - if (app.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to deploy this application", - }); - } - - updateApplication(input.applicationId as string, { - sourceType: "drop", - dropBuildPath: input.dropBuildPath, - }); - - await unzipDrop(zipFile, app); - const jobData: DeploymentJob = { - applicationId: app.applicationId, - titleLog: "Manual deployment", - descriptionLog: "", - type: "deploy", - applicationType: "application", - server: !!app.serverId, - }; - if (IS_CLOUD && app.serverId) { - jobData.serverId = app.serverId; - await deploy(jobData); - return true; - } - - await myQueue.add( - "deployments", - { ...jobData }, - { - removeOnComplete: true, - removeOnFail: true, - } - ); - return true; - }), - updateTraefikConfig: protectedProcedure - .input(z.object({ applicationId: z.string(), traefikConfig: z.string() })) - .mutation(async ({ input, ctx }) => { - const application = await findApplicationById(input.applicationId); - - if (application.project.adminId !== ctx.user.adminId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to update this application", - }); - } - - if (application.serverId) { - await writeConfigRemote( - application.serverId, - application.appName, - input.traefikConfig - ); - } else { - writeConfig(application.appName, input.traefikConfig); - } - return true; - }), - readAppMonitoring: protectedProcedure - .input(apiFindMonitoringStats) - .query(async ({ input, ctx }) => { - if (IS_CLOUD) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "Functionality not available in cloud version", - }); - } - const stats = await getApplicationStats(input.appName); - - return stats; - }), + create: protectedProcedure + .input(apiCreateApplication) + .mutation(async ({ input, ctx }) => { + try { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.authId, input.projectId, "create"); + } + + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create an application", + }); + } + + const project = await findProjectById(input.projectId); + if (project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this project", + }); + } + const newApplication = await createApplication(input); + + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, newApplication.applicationId); + } + return newApplication; + } catch (error: unknown) { + if (error instanceof TRPCError) { + throw error; + } + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the application", + cause: error, + }); + } + }), + one: protectedProcedure + .input(apiFindOneApplication) + .query(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess( + ctx.user.authId, + input.applicationId, + "access", + ); + } + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this application", + }); + } + return application; + }), + + reload: protectedProcedure + .input(apiReloadApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this application", + }); + } + if (application.serverId) { + await stopServiceRemote(application.serverId, input.appName); + } else { + await stopService(input.appName); + } + await updateApplicationStatus(input.applicationId, "idle"); + + if (application.serverId) { + await startServiceRemote(application.serverId, input.appName); + } else { + await startService(input.appName); + } + await updateApplicationStatus(input.applicationId, "done"); + return true; + }), + + delete: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess( + ctx.user.authId, + input.applicationId, + "delete", + ); + } + const application = await findApplicationById(input.applicationId); + + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to delete this application", + }); + } + + const result = await db + .delete(applications) + .where(eq(applications.applicationId, input.applicationId)) + .returning(); + + const cleanupOperations = [ + async () => await deleteAllMiddlewares(application), + async () => await removeDeployments(application), + async () => + await removeDirectoryCode(application.appName, application.serverId), + async () => + await removeMonitoringDirectory( + application.appName, + application.serverId, + ), + async () => + await removeTraefikConfig(application.appName, application.serverId), + async () => + await removeService(application?.appName, application.serverId), + ]; + + for (const operation of cleanupOperations) { + try { + await operation(); + } catch (error) {} + } + + return result[0]; + }), + + stop: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const service = await findApplicationById(input.applicationId); + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to stop this application", + }); + } + if (service.serverId) { + await stopServiceRemote(service.serverId, service.appName); + } else { + await stopService(service.appName); + } + await updateApplicationStatus(input.applicationId, "idle"); + + return service; + }), + + start: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const service = await findApplicationById(input.applicationId); + if (service.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to start this application", + }); + } + + if (service.serverId) { + await startServiceRemote(service.serverId, service.appName); + } else { + await startService(service.appName); + } + await updateApplicationStatus(input.applicationId, "done"); + + return service; + }), + + redeploy: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to redeploy this application", + }); + } + const jobData: DeploymentJob = { + applicationId: input.applicationId, + titleLog: "Rebuild deployment", + descriptionLog: "", + type: "redeploy", + applicationType: "application", + server: !!application.serverId, + }; + + if (IS_CLOUD && application.serverId) { + jobData.serverId = application.serverId; + await deploy(jobData); + return true; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + }), + saveEnvironment: protectedProcedure + .input(apiSaveEnvironmentVariables) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this environment", + }); + } + await updateApplication(input.applicationId, { + env: input.env, + buildArgs: input.buildArgs, + }); + return true; + }), + saveBuildType: protectedProcedure + .input(apiSaveBuildType) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this build type", + }); + } + await updateApplication(input.applicationId, { + buildType: input.buildType, + dockerfile: input.dockerfile, + publishDirectory: input.publishDirectory, + dockerContextPath: input.dockerContextPath, + dockerBuildStage: input.dockerBuildStage, + herokuVersion: input.herokuVersion, + }); + + return true; + }), + saveGithubProvider: protectedProcedure + .input(apiSaveGithubProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this github provider", + }); + } + await updateApplication(input.applicationId, { + repository: input.repository, + branch: input.branch, + sourceType: "github", + owner: input.owner, + buildPath: input.buildPath, + applicationStatus: "idle", + githubId: input.githubId, + }); + + return true; + }), + saveGitlabProvider: protectedProcedure + .input(apiSaveGitlabProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this gitlab provider", + }); + } + await updateApplication(input.applicationId, { + gitlabRepository: input.gitlabRepository, + gitlabOwner: input.gitlabOwner, + gitlabBranch: input.gitlabBranch, + gitlabBuildPath: input.gitlabBuildPath, + sourceType: "gitlab", + applicationStatus: "idle", + gitlabId: input.gitlabId, + gitlabProjectId: input.gitlabProjectId, + gitlabPathNamespace: input.gitlabPathNamespace, + }); + + return true; + }), + saveBitbucketProvider: protectedProcedure + .input(apiSaveBitbucketProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this bitbucket provider", + }); + } + await updateApplication(input.applicationId, { + bitbucketRepository: input.bitbucketRepository, + bitbucketOwner: input.bitbucketOwner, + bitbucketBranch: input.bitbucketBranch, + bitbucketBuildPath: input.bitbucketBuildPath, + sourceType: "bitbucket", + applicationStatus: "idle", + bitbucketId: input.bitbucketId, + }); + + return true; + }), + saveDockerProvider: protectedProcedure + .input(apiSaveDockerProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this docker provider", + }); + } + await updateApplication(input.applicationId, { + dockerImage: input.dockerImage, + username: input.username, + password: input.password, + sourceType: "docker", + applicationStatus: "idle", + registryUrl: input.registryUrl, + }); + + return true; + }), + saveGitProdiver: protectedProcedure + .input(apiSaveGitProvider) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to save this git provider", + }); + } + await updateApplication(input.applicationId, { + customGitBranch: input.customGitBranch, + customGitBuildPath: input.customGitBuildPath, + customGitUrl: input.customGitUrl, + customGitSSHKeyId: input.customGitSSHKeyId, + sourceType: "git", + applicationStatus: "idle", + }); + + return true; + }), + markRunning: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to mark this application as running", + }); + } + await updateApplicationStatus(input.applicationId, "running"); + }), + update: protectedProcedure + .input(apiUpdateApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this application", + }); + } + const { applicationId, ...rest } = input; + const updateApp = await updateApplication(applicationId, { + ...rest, + }); + + if (!updateApp) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Update: Error to update application", + }); + } + + return true; + }), + refreshToken: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to refresh this application", + }); + } + await updateApplication(input.applicationId, { + refreshToken: nanoid(), + }); + return true; + }), + deploy: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this application", + }); + } + const jobData: DeploymentJob = { + applicationId: input.applicationId, + titleLog: "Manual deployment", + descriptionLog: "", + type: "deploy", + applicationType: "application", + server: !!application.serverId, + }; + if (IS_CLOUD && application.serverId) { + jobData.serverId = application.serverId; + await deploy(jobData); + + return true; + } + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + }), + + cleanQueues: protectedProcedure + .input(apiFindOneApplication) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to clean this application", + }); + } + await cleanQueuesByApplication(input.applicationId); + }), + + readTraefikConfig: protectedProcedure + .input(apiFindOneApplication) + .query(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to read this application", + }); + } + + let traefikConfig = null; + if (application.serverId) { + traefikConfig = await readRemoteConfig( + application.serverId, + application.appName, + ); + } else { + traefikConfig = readConfig(application.appName); + } + return traefikConfig; + }), + + dropDeployment: protectedProcedure + .meta({ + openapi: { + path: "/drop-deployment", + method: "POST", + override: true, + enabled: false, + }, + }) + .use(uploadProcedure) + .input(uploadFileSchema) + .mutation(async ({ input, ctx }) => { + const zipFile = input.zip; + + const app = await findApplicationById(input.applicationId as string); + + if (app.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to deploy this application", + }); + } + + updateApplication(input.applicationId as string, { + sourceType: "drop", + dropBuildPath: input.dropBuildPath, + }); + + await unzipDrop(zipFile, app); + const jobData: DeploymentJob = { + applicationId: app.applicationId, + titleLog: "Manual deployment", + descriptionLog: "", + type: "deploy", + applicationType: "application", + server: !!app.serverId, + }; + if (IS_CLOUD && app.serverId) { + jobData.serverId = app.serverId; + await deploy(jobData); + return true; + } + + await myQueue.add( + "deployments", + { ...jobData }, + { + removeOnComplete: true, + removeOnFail: true, + }, + ); + return true; + }), + updateTraefikConfig: protectedProcedure + .input(z.object({ applicationId: z.string(), traefikConfig: z.string() })) + .mutation(async ({ input, ctx }) => { + const application = await findApplicationById(input.applicationId); + + if (application.project.adminId !== ctx.user.adminId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to update this application", + }); + } + + if (application.serverId) { + await writeConfigRemote( + application.serverId, + application.appName, + input.traefikConfig, + ); + } else { + writeConfig(application.appName, input.traefikConfig); + } + return true; + }), + readAppMonitoring: protectedProcedure + .input(apiFindMonitoringStats) + .query(async ({ input, ctx }) => { + if (IS_CLOUD) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Functionality not available in cloud version", + }); + } + const stats = await getApplicationStats(input.appName); + + return stats; + }), }); From c6892ba1882d9484c26ad601dd9493a3f1025e27 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Mon, 23 Dec 2024 08:39:50 +0100 Subject: [PATCH 50/85] format: fix formatting --- packages/server/src/services/compose.ts | 932 ++++++++++++------------ 1 file changed, 466 insertions(+), 466 deletions(-) diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index 9012cee6c..d38426fd5 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -5,42 +5,42 @@ import { type apiCreateCompose, compose } from "@dokploy/server/db/schema"; import { buildAppName, cleanAppName } from "@dokploy/server/db/schema"; import { generatePassword } from "@dokploy/server/templates/utils"; import { - buildCompose, - getBuildComposeCommand, + buildCompose, + getBuildComposeCommand, } from "@dokploy/server/utils/builders/compose"; import { randomizeSpecificationFile } from "@dokploy/server/utils/docker/compose"; import { - cloneCompose, - cloneComposeRemote, - loadDockerCompose, - loadDockerComposeRemote, + cloneCompose, + cloneComposeRemote, + loadDockerCompose, + loadDockerComposeRemote, } from "@dokploy/server/utils/docker/domain"; import type { ComposeSpecification } from "@dokploy/server/utils/docker/types"; import { sendBuildErrorNotifications } from "@dokploy/server/utils/notifications/build-error"; import { sendBuildSuccessNotifications } from "@dokploy/server/utils/notifications/build-success"; import { - execAsync, - execAsyncRemote, + execAsync, + execAsyncRemote, } from "@dokploy/server/utils/process/execAsync"; import { - cloneBitbucketRepository, - getBitbucketCloneCommand, + cloneBitbucketRepository, + getBitbucketCloneCommand, } from "@dokploy/server/utils/providers/bitbucket"; import { - cloneGitRepository, - getCustomGitCloneCommand, + cloneGitRepository, + getCustomGitCloneCommand, } from "@dokploy/server/utils/providers/git"; import { - cloneGithubRepository, - getGithubCloneCommand, + cloneGithubRepository, + getGithubCloneCommand, } from "@dokploy/server/utils/providers/github"; import { - cloneGitlabRepository, - getGitlabCloneCommand, + cloneGitlabRepository, + getGitlabCloneCommand, } from "@dokploy/server/utils/providers/gitlab"; import { - createComposeFile, - getCreateComposeFileCommand, + createComposeFile, + getCreateComposeFileCommand, } from "@dokploy/server/utils/providers/raw"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; @@ -52,501 +52,501 @@ import { validUniqueServerAppName } from "./project"; export type Compose = typeof compose.$inferSelect; export const createCompose = async (input: typeof apiCreateCompose._type) => { - const appName = buildAppName("compose", input.appName); - - const valid = await validUniqueServerAppName(appName); - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } - - const newDestination = await db - .insert(compose) - .values({ - ...input, - composeFile: "", - appName, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting compose", - }); - } - - return newDestination; + const appName = buildAppName("compose", input.appName); + + const valid = await validUniqueServerAppName(appName); + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + + const newDestination = await db + .insert(compose) + .values({ + ...input, + composeFile: "", + appName, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting compose", + }); + } + + return newDestination; }; export const createComposeByTemplate = async ( - input: typeof compose.$inferInsert + input: typeof compose.$inferInsert, ) => { - const appName = cleanAppName(input.appName); - if (appName) { - const valid = await validUniqueServerAppName(appName); - - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } - } - const newDestination = await db - .insert(compose) - .values({ - ...input, - appName, - }) - .returning() - .then((value) => value[0]); - - if (!newDestination) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Inserting compose", - }); - } - - return newDestination; + const appName = cleanAppName(input.appName); + if (appName) { + const valid = await validUniqueServerAppName(appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newDestination = await db + .insert(compose) + .values({ + ...input, + appName, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting compose", + }); + } + + return newDestination; }; export const findComposeById = async (composeId: string) => { - const result = await db.query.compose.findFirst({ - where: eq(compose.composeId, composeId), - with: { - project: true, - deployments: true, - mounts: true, - domains: true, - github: true, - gitlab: true, - bitbucket: true, - server: true, - }, - }); - if (!result) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Compose not found", - }); - } - return result; + const result = await db.query.compose.findFirst({ + where: eq(compose.composeId, composeId), + with: { + project: true, + deployments: true, + mounts: true, + domains: true, + github: true, + gitlab: true, + bitbucket: true, + server: true, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Compose not found", + }); + } + return result; }; export const loadServices = async ( - composeId: string, - type: "fetch" | "cache" = "fetch" + composeId: string, + type: "fetch" | "cache" = "fetch", ) => { - const compose = await findComposeById(composeId); - - if (type === "fetch") { - if (compose.serverId) { - await cloneComposeRemote(compose); - } else { - await cloneCompose(compose); - } - } - - let composeData: ComposeSpecification | null; - - if (compose.serverId) { - composeData = await loadDockerComposeRemote(compose); - } else { - composeData = await loadDockerCompose(compose); - } - - if (compose.randomize && composeData) { - const randomizedCompose = randomizeSpecificationFile( - composeData, - compose.suffix - ); - composeData = randomizedCompose; - } - - if (!composeData?.services) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Services not found", - }); - } - - const services = Object.keys(composeData.services); - - return [...services]; + const compose = await findComposeById(composeId); + + if (type === "fetch") { + if (compose.serverId) { + await cloneComposeRemote(compose); + } else { + await cloneCompose(compose); + } + } + + let composeData: ComposeSpecification | null; + + if (compose.serverId) { + composeData = await loadDockerComposeRemote(compose); + } else { + composeData = await loadDockerCompose(compose); + } + + if (compose.randomize && composeData) { + const randomizedCompose = randomizeSpecificationFile( + composeData, + compose.suffix, + ); + composeData = randomizedCompose; + } + + if (!composeData?.services) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Services not found", + }); + } + + const services = Object.keys(composeData.services); + + return [...services]; }; export const updateCompose = async ( - composeId: string, - composeData: Partial + composeId: string, + composeData: Partial, ) => { - const { appName, ...rest } = composeData; - const composeResult = await db - .update(compose) - .set({ - ...rest, - }) - .where(eq(compose.composeId, composeId)) - .returning(); - - return composeResult[0]; + const { appName, ...rest } = composeData; + const composeResult = await db + .update(compose) + .set({ + ...rest, + }) + .where(eq(compose.composeId, composeId)) + .returning(); + + return composeResult[0]; }; export const deployCompose = async ({ - composeId, - titleLog = "Manual deployment", - descriptionLog = "", + composeId, + titleLog = "Manual deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const buildLink = `${await getDokployUrl()}/dashboard/project/${ - compose.projectId - }/services/compose/${compose.composeId}?tab=deployments`; - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.sourceType === "github") { - await cloneGithubRepository({ - ...compose, - logPath: deployment.logPath, - type: "compose", - }); - } else if (compose.sourceType === "gitlab") { - await cloneGitlabRepository(compose, deployment.logPath, true); - } else if (compose.sourceType === "bitbucket") { - await cloneBitbucketRepository(compose, deployment.logPath, true); - } else if (compose.sourceType === "git") { - await cloneGitRepository(compose, deployment.logPath, true); - } else if (compose.sourceType === "raw") { - await createComposeFile(compose, deployment.logPath); - } - await buildCompose(compose, deployment.logPath); - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - - await sendBuildSuccessNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - buildLink, - adminId: compose.project.adminId, - }); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - await sendBuildErrorNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - // @ts-ignore - errorMessage: error?.message || "Error to build", - buildLink, - adminId: compose.project.adminId, - }); - throw error; - } + const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${ + compose.projectId + }/services/compose/${compose.composeId}?tab=deployments`; + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.sourceType === "github") { + await cloneGithubRepository({ + ...compose, + logPath: deployment.logPath, + type: "compose", + }); + } else if (compose.sourceType === "gitlab") { + await cloneGitlabRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "bitbucket") { + await cloneBitbucketRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "git") { + await cloneGitRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "raw") { + await createComposeFile(compose, deployment.logPath); + } + await buildCompose(compose, deployment.logPath); + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + + await sendBuildSuccessNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + buildLink, + adminId: compose.project.adminId, + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + await sendBuildErrorNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + adminId: compose.project.adminId, + }); + throw error; + } }; export const rebuildCompose = async ({ - composeId, - titleLog = "Rebuild deployment", - descriptionLog = "", + composeId, + titleLog = "Rebuild deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.serverId) { - await getBuildComposeCommand(compose, deployment.logPath); - } else { - await buildCompose(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; + const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.serverId) { + await getBuildComposeCommand(compose, deployment.logPath); + } else { + await buildCompose(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; }; export const deployRemoteCompose = async ({ - composeId, - titleLog = "Manual deployment", - descriptionLog = "", + composeId, + titleLog = "Manual deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const buildLink = `${await getDokployUrl()}/dashboard/project/${ - compose.projectId - }/services/compose/${compose.composeId}?tab=deployments`; - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - try { - if (compose.serverId) { - let command = "set -e;"; - - if (compose.sourceType === "github") { - command += await getGithubCloneCommand({ - ...compose, - logPath: deployment.logPath, - type: "compose", - serverId: compose.serverId, - }); - } else if (compose.sourceType === "gitlab") { - command += await getGitlabCloneCommand( - compose, - deployment.logPath, - true - ); - } else if (compose.sourceType === "bitbucket") { - command += await getBitbucketCloneCommand( - compose, - deployment.logPath, - true - ); - } else if (compose.sourceType === "git") { - command += await getCustomGitCloneCommand( - compose, - deployment.logPath, - true - ); - } else if (compose.sourceType === "raw") { - command += getCreateComposeFileCommand(compose, deployment.logPath); - } - - await execAsyncRemote(compose.serverId, command); - await getBuildComposeCommand(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - - await sendBuildSuccessNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - buildLink, - adminId: compose.project.adminId, - }); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - compose.serverId, - ` + const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${ + compose.projectId + }/services/compose/${compose.composeId}?tab=deployments`; + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + try { + if (compose.serverId) { + let command = "set -e;"; + + if (compose.sourceType === "github") { + command += await getGithubCloneCommand({ + ...compose, + logPath: deployment.logPath, + type: "compose", + serverId: compose.serverId, + }); + } else if (compose.sourceType === "gitlab") { + command += await getGitlabCloneCommand( + compose, + deployment.logPath, + true, + ); + } else if (compose.sourceType === "bitbucket") { + command += await getBitbucketCloneCommand( + compose, + deployment.logPath, + true, + ); + } else if (compose.sourceType === "git") { + command += await getCustomGitCloneCommand( + compose, + deployment.logPath, + true, + ); + } else if (compose.sourceType === "raw") { + command += getCreateComposeFileCommand(compose, deployment.logPath); + } + + await execAsyncRemote(compose.serverId, command); + await getBuildComposeCommand(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + + await sendBuildSuccessNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + buildLink, + adminId: compose.project.adminId, + }); + } catch (error) { + // @ts-ignore + const encodedContent = encodeBase64(error?.message); + + await execAsyncRemote( + compose.serverId, + ` echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";` - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - await sendBuildErrorNotifications({ - projectName: compose.project.name, - applicationName: compose.name, - applicationType: "compose", - // @ts-ignore - errorMessage: error?.message || "Error to build", - buildLink, - adminId: compose.project.adminId, - }); - throw error; - } + echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, + ); + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + await sendBuildErrorNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + adminId: compose.project.adminId, + }); + throw error; + } }; export const rebuildRemoteCompose = async ({ - composeId, - titleLog = "Rebuild deployment", - descriptionLog = "", + composeId, + titleLog = "Rebuild deployment", + descriptionLog = "", }: { - composeId: string; - titleLog: string; - descriptionLog: string; + composeId: string; + titleLog: string; + descriptionLog: string; }) => { - const compose = await findComposeById(composeId); - const deployment = await createDeploymentCompose({ - composeId: composeId, - title: titleLog, - description: descriptionLog, - }); - - try { - if (compose.serverId) { - await getBuildComposeCommand(compose, deployment.logPath); - } - - await updateDeploymentStatus(deployment.deploymentId, "done"); - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - // @ts-ignore - const encodedContent = encodeBase64(error?.message); - - await execAsyncRemote( - compose.serverId, - ` + const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.serverId) { + await getBuildComposeCommand(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + // @ts-ignore + const encodedContent = encodeBase64(error?.message); + + await execAsyncRemote( + compose.serverId, + ` echo "\n\n===================================EXTRA LOGS============================================" >> ${deployment.logPath}; echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath}; - echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";` - ); - await updateDeploymentStatus(deployment.deploymentId, "error"); - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; + echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`, + ); + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; }; export const removeCompose = async ( - compose: Compose, - deleteVolumes: boolean + compose: Compose, + deleteVolumes: boolean, ) => { - try { - const { COMPOSE_PATH } = paths(!!compose.serverId); - const projectPath = join(COMPOSE_PATH, compose.appName); - - console.log("API: DELETE VOLUMES=", deleteVolumes); - - if (compose.composeType === "stack") { - const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; - - if (compose.serverId) { - await execAsyncRemote(compose.serverId, command); - } else { - await execAsync(command); - } - await execAsync(command, { - cwd: projectPath, - }); - } else { - let command: string; - if (deleteVolumes) { - command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`; - } else { - command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`; - } - - if (compose.serverId) { - await execAsyncRemote(compose.serverId, command); - } else { - await execAsync(command, { - cwd: projectPath, - }); - } - } - } catch (error) { - throw error; - } - - return true; + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + const projectPath = join(COMPOSE_PATH, compose.appName); + + console.log("API: DELETE VOLUMES=", deleteVolumes); + + if (compose.composeType === "stack") { + const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; + + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } + await execAsync(command, { + cwd: projectPath, + }); + } else { + let command: string; + if (deleteVolumes) { + command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`; + } else { + command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`; + } + + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command, { + cwd: projectPath, + }); + } + } + } catch (error) { + throw error; + } + + return true; }; export const startCompose = async (composeId: string) => { - const compose = await findComposeById(composeId); - try { - const { COMPOSE_PATH } = paths(!!compose.serverId); - if (compose.composeType === "docker-compose") { - if (compose.serverId) { - await execAsyncRemote( - compose.serverId, - `cd ${join( - COMPOSE_PATH, - compose.appName, - "code" - )} && docker compose -p ${compose.appName} up -d` - ); - } else { - await execAsync(`docker compose -p ${compose.appName} up -d`, { - cwd: join(COMPOSE_PATH, compose.appName, "code"), - }); - } - } - - await updateCompose(composeId, { - composeStatus: "done", - }); - } catch (error) { - await updateCompose(composeId, { - composeStatus: "idle", - }); - throw error; - } - - return true; + const compose = await findComposeById(composeId); + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + if (compose.composeType === "docker-compose") { + if (compose.serverId) { + await execAsyncRemote( + compose.serverId, + `cd ${join( + COMPOSE_PATH, + compose.appName, + "code", + )} && docker compose -p ${compose.appName} up -d`, + ); + } else { + await execAsync(`docker compose -p ${compose.appName} up -d`, { + cwd: join(COMPOSE_PATH, compose.appName, "code"), + }); + } + } + + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + await updateCompose(composeId, { + composeStatus: "idle", + }); + throw error; + } + + return true; }; export const stopCompose = async (composeId: string) => { - const compose = await findComposeById(composeId); - try { - const { COMPOSE_PATH } = paths(!!compose.serverId); - if (compose.composeType === "docker-compose") { - if (compose.serverId) { - await execAsyncRemote( - compose.serverId, - `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${ - compose.appName - } stop` - ); - } else { - await execAsync(`docker compose -p ${compose.appName} stop`, { - cwd: join(COMPOSE_PATH, compose.appName), - }); - } - } - - await updateCompose(composeId, { - composeStatus: "idle", - }); - } catch (error) { - await updateCompose(composeId, { - composeStatus: "error", - }); - throw error; - } - - return true; + const compose = await findComposeById(composeId); + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + if (compose.composeType === "docker-compose") { + if (compose.serverId) { + await execAsyncRemote( + compose.serverId, + `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${ + compose.appName + } stop`, + ); + } else { + await execAsync(`docker compose -p ${compose.appName} stop`, { + cwd: join(COMPOSE_PATH, compose.appName), + }); + } + } + + await updateCompose(composeId, { + composeStatus: "idle", + }); + } catch (error) { + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; }; From ed543e5397bca2af1e10e9e8c7d153251d6bda45 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 23 Dec 2024 02:28:40 -0600 Subject: [PATCH 51/85] refactor: lint --- .../deployments/show-deployment.tsx | 58 +- .../show-preview-builds.tsx | 4 +- .../show-preview-deployments.tsx | 420 ++++----- .../show-preview-settings.tsx | 14 +- .../deployments/show-deployment-compose.tsx | 53 +- .../compose/general/deploy-compose.tsx | 2 +- .../docker/terminal/docker-terminal-modal.tsx | 2 +- .../components/dashboard/projects/show.tsx | 536 +++++------ .../components/dashboard/search-command.tsx | 342 +++---- .../notifications/delete-notification.tsx | 8 +- .../notifications/show-notifications.tsx | 102 ++- .../notifications/update-notification.tsx | 8 +- .../settings/profile/profile-form.tsx | 2 +- .../settings/profile/remove-self-account.tsx | 6 +- .../servers/actions/show-traefik-actions.tsx | 2 +- .../settings/servers/edit-script.tsx | 3 +- .../settings/servers/setup-server.tsx | 2 +- .../settings/servers/show-servers.tsx | 4 +- .../servers/welcome-stripe/create-ssh-key.tsx | 6 +- .../settings/servers/welcome-stripe/setup.tsx | 18 +- .../servers/welcome-stripe/verify.tsx | 14 +- .../welcome-stripe/welcome-suscription.tsx | 14 +- .../web-server/docker-terminal-modal.tsx | 2 +- .../web-server/manage-traefik-ports.tsx | 2 +- apps/dokploy/pages/api/deploy/github.ts | 6 +- apps/dokploy/public/locales/it/common.json | 2 +- apps/dokploy/public/locales/it/settings.json | 88 +- apps/dokploy/templates/onedev/index.ts | 30 +- apps/dokploy/templates/unsend/index.ts | 72 +- packages/server/src/db/schema/application.ts | 826 ++++++++--------- packages/server/src/db/schema/deployment.ts | 2 +- packages/server/src/db/schema/domain.ts | 2 +- packages/server/src/db/schema/index.ts | 2 +- .../src/db/schema/preview-deployments.ts | 8 +- packages/server/src/utils/backups/index.ts | 4 +- packages/server/src/utils/builders/index.ts | 2 +- packages/server/src/utils/docker/utils.ts | 858 +++++++++--------- 37 files changed, 1762 insertions(+), 1764 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 380b22d93..c67d1ba60 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, @@ -5,11 +6,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { TerminalLine } from "../../docker/logs/terminal-line"; -import { LogLine, parseLogs } from "../../docker/logs/utils"; -import { Badge } from "@/components/ui/badge"; -import { Loader2 } from "lucide-react"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; @@ -24,21 +24,20 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { const [autoScroll, setAutoScroll] = useState(true); const scrollRef = useRef(null); - const scrollToBottom = () => { if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }; - + }; + const handleScroll = () => { if (!scrollRef.current) return; - + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; setAutoScroll(isAtBottom); - }; - + }; + useEffect(() => { if (!open || !logPath) return; @@ -69,7 +68,6 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { }; }, [logPath, open]); - useEffect(() => { const logs = parseLogs(data); setFilteredLogs(logs); @@ -77,12 +75,11 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { useEffect(() => { scrollToBottom(); - + if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }, [filteredLogs, autoScroll]); - + }, [filteredLogs, autoScroll]); return ( { Deployment - See all the details of this deployment | {filteredLogs.length} lines + See all the details of this deployment |{" "} + + {filteredLogs.length} lines + -
{ - filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( - - )) : - ( -
- -
- )} + > + {" "} + {filteredLogs.length > 0 ? ( + filteredLogs.map((log: LogLine, index: number) => ( + + )) + ) : ( +
+ +
+ )}
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx index bff6c9290..154510c3b 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx @@ -26,7 +26,9 @@ export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => { return ( - + diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 898d412a0..c2d712c3b 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,237 +1,237 @@ -import React from "react"; +import { DateTooltip } from "@/components/shared/date-tooltip"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; +import { api } from "@/utils/api"; import { - Clock, - GitBranch, - GitPullRequest, - Pencil, - RocketIcon, + Clock, + GitBranch, + GitPullRequest, + Pencil, + RocketIcon, } from "lucide-react"; import Link from "next/link"; -import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; -import { DialogAction } from "@/components/shared/dialog-action"; -import { api } from "@/utils/api"; -import { ShowPreviewBuilds } from "./show-preview-builds"; -import { DateTooltip } from "@/components/shared/date-tooltip"; +import React from "react"; import { toast } from "sonner"; -import { StatusTooltip } from "@/components/shared/status-tooltip"; +import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { AddPreviewDomain } from "./add-preview-domain"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { ShowPreviewBuilds } from "./show-preview-builds"; import { ShowPreviewSettings } from "./show-preview-settings"; interface Props { - applicationId: string; + applicationId: string; } export const ShowPreviewDeployments = ({ applicationId }: Props) => { - const { data } = api.application.one.useQuery({ applicationId }); + const { data } = api.application.one.useQuery({ applicationId }); - const { mutateAsync: deletePreviewDeployment, isLoading } = - api.previewDeployment.delete.useMutation(); + const { mutateAsync: deletePreviewDeployment, isLoading } = + api.previewDeployment.delete.useMutation(); - const { data: previewDeployments, refetch: refetchPreviewDeployments } = - api.previewDeployment.all.useQuery( - { applicationId }, - { - enabled: !!applicationId, - } - ); + const { data: previewDeployments, refetch: refetchPreviewDeployments } = + api.previewDeployment.all.useQuery( + { applicationId }, + { + enabled: !!applicationId, + }, + ); - const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { - deletePreviewDeployment({ - previewDeploymentId: previewDeploymentId, - }) - .then(() => { - refetchPreviewDeployments(); - toast.success("Preview deployment deleted"); - }) - .catch((error) => { - toast.error(error.message); - }); - }; + const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { + deletePreviewDeployment({ + previewDeploymentId: previewDeploymentId, + }) + .then(() => { + refetchPreviewDeployments(); + toast.success("Preview deployment deleted"); + }) + .catch((error) => { + toast.error(error.message); + }); + }; - return ( - - -
- Preview Deployments - See all the preview deployments -
- {data?.isPreviewDeploymentsActive && ( - - )} -
- - {data?.isPreviewDeploymentsActive ? ( - <> -
- - Preview deployments are a way to test your application before it - is deployed to production. It will create a new deployment for - each pull request you create. - -
- {!previewDeployments?.length ? ( -
- - - No preview deployments found - -
- ) : ( -
- {previewDeployments.map((previewDeployment) => ( -
-
- - {previewDeployment.pullRequestTitle} - - - - {previewDeployment.previewStatus - ?.replace("running", "Running") - .replace("done", "Done") - .replace("error", "Error") - .replace("idle", "Idle") || "Idle"} - -
+ return ( + + +
+ Preview Deployments + See all the preview deployments +
+ {data?.isPreviewDeploymentsActive && ( + + )} +
+ + {data?.isPreviewDeploymentsActive ? ( + <> +
+ + Preview deployments are a way to test your application before it + is deployed to production. It will create a new deployment for + each pull request you create. + +
+ {!previewDeployments?.length ? ( +
+ + + No preview deployments found + +
+ ) : ( +
+ {previewDeployments.map((previewDeployment) => ( +
+
+ + {previewDeployment.pullRequestTitle} + + + + {previewDeployment.previewStatus + ?.replace("running", "Running") + .replace("done", "Done") + .replace("error", "Error") + .replace("idle", "Idle") || "Idle"} + +
-
-
- - {previewDeployment.domain?.host} - +
+
+ + {previewDeployment.domain?.host} + - - - -
+ + + +
-
-
- - Branch: - - {previewDeployment.branch} - -
-
- - Deployed: - - - -
-
+
+
+ + Branch: + + {previewDeployment.branch} + +
+
+ + Deployed: + + + +
+
- + -
-

- Pull Request -

-
- - - {previewDeployment.pullRequestTitle} - -
-
-
+
+

+ Pull Request +

+
+ + + {previewDeployment.pullRequestTitle} + +
+
+
-
-
- - - +
+
+ + + - + - - handleDeletePreviewDeployment( - previewDeployment.previewDeploymentId - ) - } - > - - -
-
-
- ))} -
- )} - - ) : ( -
- - - Preview deployments are disabled for this application, please - enable it - - -
- )} - - - ); + + handleDeletePreviewDeployment( + previewDeployment.previewDeploymentId, + ) + } + > + + +
+
+
+ ))} +
+ )} + + ) : ( +
+ + + Preview deployments are disabled for this application, please + enable it + + +
+ )} +
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index 6e56bbdd0..0e4231eb4 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -1,5 +1,3 @@ -import { api } from "@/utils/api"; -import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -20,12 +18,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input, NumberInput } from "@/components/ui/input"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; import { Secrets } from "@/components/ui/secrets"; -import { toast } from "sonner"; -import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, @@ -33,6 +26,13 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; const schema = z.object({ env: z.string(), diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx index 4a45fb204..d683d4ab4 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, @@ -5,12 +6,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { TerminalLine } from "../../docker/logs/terminal-line"; -import { LogLine, parseLogs } from "../../docker/logs/utils"; -import { Badge } from "@/components/ui/badge"; -import { Loader2 } from "lucide-react"; - +import { type LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; @@ -32,19 +31,18 @@ export const ShowDeploymentCompose = ({ const scrollToBottom = () => { if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }; - + }; + const handleScroll = () => { if (!scrollRef.current) return; - + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; setAutoScroll(isAtBottom); - }; - - + }; + useEffect(() => { if (!open || !logPath) return; @@ -76,7 +74,6 @@ export const ShowDeploymentCompose = ({ }; }, [logPath, open]); - useEffect(() => { const logs = parseLogs(data); setFilteredLogs(logs); @@ -84,11 +81,11 @@ export const ShowDeploymentCompose = ({ useEffect(() => { scrollToBottom(); - + if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }, [filteredLogs, autoScroll]); + }, [filteredLogs, autoScroll]); return ( Deployment - See all the details of this deployment | {filteredLogs.length} lines + See all the details of this deployment |{" "} + + {filteredLogs.length} lines +
-
- - - { - filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( - - )) : - ( + {filteredLogs.length > 0 ? ( + filteredLogs.map((log: LogLine, index: number) => ( + + )) + ) : (
- ) - } + )}
diff --git a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx b/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx index c02a78028..25b59cc73 100644 --- a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx @@ -53,7 +53,7 @@ export const DeployCompose = ({ composeId }: Props) => { }) .then(async () => { router.push( - `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments` + `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`, ); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx index e7301b0aa..90aa2b406 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx @@ -59,7 +59,7 @@ export const DockerTerminalModal = ({ {children} - event.preventDefault()} > diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index d05bbba2c..d4d9ac553 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -1,35 +1,35 @@ import { DateTooltip } from "@/components/shared/date-tooltip"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { - AlertTriangle, - BookIcon, - ExternalLink, - ExternalLinkIcon, - FolderInput, - MoreHorizontalIcon, - TrashIcon, + AlertTriangle, + BookIcon, + ExternalLink, + ExternalLinkIcon, + FolderInput, + MoreHorizontalIcon, + TrashIcon, } from "lucide-react"; import Link from "next/link"; import { Fragment } from "react"; @@ -38,257 +38,257 @@ import { ProjectEnviroment } from "./project-enviroment"; import { UpdateProject } from "./update"; export const ShowProjects = () => { - const utils = api.useUtils(); - const { data } = api.project.all.useQuery(); - const { data: auth } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - } - ); - const { mutateAsync } = api.project.remove.useMutation(); + const utils = api.useUtils(); + const { data } = api.project.all.useQuery(); + const { data: auth } = api.auth.get.useQuery(); + const { data: user } = api.user.byAuthId.useQuery( + { + authId: auth?.id || "", + }, + { + enabled: !!auth?.id && auth?.rol === "user", + }, + ); + const { mutateAsync } = api.project.remove.useMutation(); - return ( - <> - {data?.length === 0 && ( -
- - - No projects added yet. Click on Create project. - -
- )} -
- {data?.map((project) => { - const emptyServices = - project?.mariadb.length === 0 && - project?.mongo.length === 0 && - project?.mysql.length === 0 && - project?.postgres.length === 0 && - project?.redis.length === 0 && - project?.applications.length === 0 && - project?.compose.length === 0; + return ( + <> + {data?.length === 0 && ( +
+ + + No projects added yet. Click on Create project. + +
+ )} +
+ {data?.map((project) => { + const emptyServices = + project?.mariadb.length === 0 && + project?.mongo.length === 0 && + project?.mysql.length === 0 && + project?.postgres.length === 0 && + project?.redis.length === 0 && + project?.applications.length === 0 && + project?.compose.length === 0; - const totalServices = - project?.mariadb.length + - project?.mongo.length + - project?.mysql.length + - project?.postgres.length + - project?.redis.length + - project?.applications.length + - project?.compose.length; + const totalServices = + project?.mariadb.length + + project?.mongo.length + + project?.mysql.length + + project?.postgres.length + + project?.redis.length + + project?.applications.length + + project?.compose.length; - const flattedDomains = [ - ...project.applications.flatMap((a) => a.domains), - ...project.compose.flatMap((a) => a.domains), - ]; + const flattedDomains = [ + ...project.applications.flatMap((a) => a.domains), + ...project.compose.flatMap((a) => a.domains), + ]; - const renderDomainsDropdown = ( - item: typeof project.compose | typeof project.applications - ) => - item[0] ? ( - - - {"applicationId" in item[0] ? "Applications" : "Compose"} - - {item.map((a) => ( - - - - - {a.name} - - - {a.domains.map((domain) => ( - - - {domain.host} - - - - ))} - - - ))} - - ) : null; + const renderDomainsDropdown = ( + item: typeof project.compose | typeof project.applications, + ) => + item[0] ? ( + + + {"applicationId" in item[0] ? "Applications" : "Compose"} + + {item.map((a) => ( + + + + + {a.name} + + + {a.domains.map((domain) => ( + + + {domain.host} + + + + ))} + + + ))} + + ) : null; - return ( -
- - - {flattedDomains.length > 1 ? ( - - - - - e.stopPropagation()} - > - {renderDomainsDropdown(project.applications)} - {renderDomainsDropdown(project.compose)} - - - ) : flattedDomains[0] ? ( - - ) : null} + return ( +
+ + + {flattedDomains.length > 1 ? ( + + + + + e.stopPropagation()} + > + {renderDomainsDropdown(project.applications)} + {renderDomainsDropdown(project.compose)} + + + ) : flattedDomains[0] ? ( + + ) : null} - - - -
- - - {project.name} - -
+ + + +
+ + + {project.name} + +
- - {project.description} - -
-
- - - - - - - Actions - -
e.stopPropagation()}> - -
-
e.stopPropagation()}> - -
+ + {project.description} + + +
+ + + + + + + Actions + +
e.stopPropagation()}> + +
+
e.stopPropagation()}> + +
-
e.stopPropagation()}> - {(auth?.rol === "admin" || - user?.canDeleteProjects) && ( - - - e.preventDefault()} - > - - Delete - - - - - - Are you sure to delete this project? - - {!emptyServices ? ( -
- - - You have active services, please - delete them first - -
- ) : ( - - This action cannot be undone - - )} -
- - - Cancel - - { - await mutateAsync({ - projectId: project.projectId, - }) - .then(() => { - toast.success( - "Project delete succesfully" - ); - }) - .catch(() => { - toast.error( - "Error to delete this project" - ); - }) - .finally(() => { - utils.project.all.invalidate(); - }); - }} - > - Delete - - -
-
- )} -
-
-
-
- - - -
- - Created - - - {totalServices}{" "} - {totalServices === 1 ? "service" : "services"} - -
-
- - -
- ); - })} -
- - ); +
e.stopPropagation()}> + {(auth?.rol === "admin" || + user?.canDeleteProjects) && ( + + + e.preventDefault()} + > + + Delete + + + + + + Are you sure to delete this project? + + {!emptyServices ? ( +
+ + + You have active services, please + delete them first + +
+ ) : ( + + This action cannot be undone + + )} +
+ + + Cancel + + { + await mutateAsync({ + projectId: project.projectId, + }) + .then(() => { + toast.success( + "Project delete succesfully", + ); + }) + .catch(() => { + toast.error( + "Error to delete this project", + ); + }) + .finally(() => { + utils.project.all.invalidate(); + }); + }} + > + Delete + + +
+
+ )} +
+ + +
+ + + +
+ + Created + + + {totalServices}{" "} + {totalServices === 1 ? "service" : "services"} + +
+
+ + +
+ ); + })} +
+ + ); }; diff --git a/apps/dokploy/components/dashboard/search-command.tsx b/apps/dokploy/components/dashboard/search-command.tsx index 8afea6720..4d3c75f98 100644 --- a/apps/dokploy/components/dashboard/search-command.tsx +++ b/apps/dokploy/components/dashboard/search-command.tsx @@ -1,189 +1,189 @@ "use client"; -import React from "react"; import { - Command, - CommandEmpty, - CommandList, - CommandGroup, - CommandInput, - CommandItem, - CommandDialog, - CommandSeparator, + MariadbIcon, + MongodbIcon, + MysqlIcon, + PostgresqlIcon, + RedisIcon, +} from "@/components/icons/data-tools-icons"; +import { Badge } from "@/components/ui/badge"; +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, } from "@/components/ui/command"; -import { useRouter } from "next/router"; import { - extractServices, - type Services, + type Services, + extractServices, } from "@/pages/dashboard/project/[projectId]"; +import { api } from "@/utils/api"; import type { findProjectById } from "@dokploy/server/services/project"; import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react"; -import { - MariadbIcon, - MongodbIcon, - MysqlIcon, - PostgresqlIcon, - RedisIcon, -} from "@/components/icons/data-tools-icons"; -import { api } from "@/utils/api"; -import { Badge } from "@/components/ui/badge"; +import { useRouter } from "next/router"; +import React from "react"; import { StatusTooltip } from "../shared/status-tooltip"; type Project = Awaited>; export const SearchCommand = () => { - const router = useRouter(); - const [open, setOpen] = React.useState(false); - const [search, setSearch] = React.useState(""); + const router = useRouter(); + const [open, setOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); - const { data } = api.project.all.useQuery(); - const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); + const { data } = api.project.all.useQuery(); + const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); - React.useEffect(() => { - const down = (e: KeyboardEvent) => { - if (e.key === "j" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - setOpen((open) => !open); - } - }; + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "j" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; - document.addEventListener("keydown", down); - return () => document.removeEventListener("keydown", down); - }, []); + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); - return ( -
- - - - - No projects added yet. Click on Create project. - - - - {data?.map((project) => ( - { - router.push(`/dashboard/project/${project.projectId}`); - setOpen(false); - }} - > - - {project.name} - - ))} - - - - - - {data?.map((project) => { - const applications: Services[] = extractServices(project); - return applications.map((application) => ( - { - router.push( - `/dashboard/project/${project.projectId}/services/${application.type}/${application.id}` - ); - setOpen(false); - }} - > - {application.type === "postgres" && ( - - )} - {application.type === "redis" && ( - - )} - {application.type === "mariadb" && ( - - )} - {application.type === "mongo" && ( - - )} - {application.type === "mysql" && ( - - )} - {application.type === "application" && ( - - )} - {application.type === "compose" && ( - - )} - - {project.name} / {application.name}{" "} -
{application.id}
-
-
- -
-
- )); - })} -
-
- - -
-
-
- ); + return ( +
+ + + + + No projects added yet. Click on Create project. + + + + {data?.map((project) => ( + { + router.push(`/dashboard/project/${project.projectId}`); + setOpen(false); + }} + > + + {project.name} + + ))} + + + + + + {data?.map((project) => { + const applications: Services[] = extractServices(project); + return applications.map((application) => ( + { + router.push( + `/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`, + ); + setOpen(false); + }} + > + {application.type === "postgres" && ( + + )} + {application.type === "redis" && ( + + )} + {application.type === "mariadb" && ( + + )} + {application.type === "mongo" && ( + + )} + {application.type === "mysql" && ( + + )} + {application.type === "application" && ( + + )} + {application.type === "compose" && ( + + )} + + {project.name} / {application.name}{" "} +
{application.id}
+
+
+ +
+
+ )); + })} +
+
+ + +
+
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx index 4bb197b29..02c95d8b3 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx @@ -24,12 +24,12 @@ export const DeleteNotification = ({ notificationId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 2b5d02e0e..8a45bbf31 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -40,58 +40,60 @@ export const ShowNotifications = () => {
) : (
-
- {data?.map((notification, index) => ( -
-
- {notification.notificationType === "slack" && ( -
- -
- )} - {notification.notificationType === "telegram" && ( -
- -
- )} - {notification.notificationType === "discord" && ( -
- -
- )} - {notification.notificationType === "email" && ( -
- -
- )} -
- - {notification.name} - - - {notification.notificationType?.[0]?.toUpperCase() + notification.notificationType?.slice(1)} notification - -
-
-
- - -
+
+ {data?.map((notification, index) => ( +
+
+ {notification.notificationType === "slack" && ( +
+ +
+ )} + {notification.notificationType === "telegram" && ( +
+ +
+ )} + {notification.notificationType === "discord" && ( +
+ +
+ )} + {notification.notificationType === "email" && ( +
+ +
+ )} +
+ + {notification.name} + + + {notification.notificationType?.[0]?.toUpperCase() + + notification.notificationType?.slice(1)}{" "} + notification + +
+
+
+ + +
+
+ ))} +
+ +
+
- ))} -
- -
-
-
)} diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx index 5c594dc49..41306385d 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx @@ -218,9 +218,11 @@ export const UpdateNotification = ({ notificationId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 1141397f9..191c1936d 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Card, @@ -26,7 +27,6 @@ import { toast } from "sonner"; import { z } from "zod"; import { Disable2FA } from "./disable-2fa"; import { Enable2FA } from "./enable-2fa"; -import { AlertBlock } from "@/components/shared/alert-block"; const profileSchema = z.object({ email: z.string(), diff --git a/apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx b/apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx index f4f4680bc..3fc554522 100644 --- a/apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/remove-self-account.tsx @@ -1,3 +1,5 @@ +import { AlertBlock } from "@/components/shared/alert-block"; +import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { Card, @@ -18,13 +20,11 @@ import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; -import { DialogAction } from "@/components/shared/dialog-action"; -import { AlertBlock } from "@/components/shared/alert-block"; -import { useRouter } from "next/router"; const profileSchema = z.object({ password: z.string().min(1, { diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx index 546069c5b..72854f930 100644 --- a/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/actions/show-traefik-actions.tsx @@ -25,8 +25,8 @@ import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { useTranslation } from "next-i18next"; import { EditTraefikEnv } from "../../web-server/edit-traefik-env"; -import { ShowModalLogs } from "../../web-server/show-modal-logs"; import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports"; +import { ShowModalLogs } from "../../web-server/show-modal-logs"; interface Props { serverId?: string; diff --git a/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx b/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx index faaecb1fa..0a22220ed 100644 --- a/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/edit-script.tsx @@ -108,7 +108,8 @@ export const EditScript = ({ serverId }: Props) => { - We recommend not modifying this script unless you know what you are doing. + We recommend not modifying this script unless you know what you are + doing.
diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index 7c1814591..252ca16c5 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -34,8 +34,8 @@ import { toast } from "sonner"; import { ShowDeployment } from "../../application/deployments/show-deployment"; import { EditScript } from "./edit-script"; import { GPUSupport } from "./gpu-support"; -import { ValidateServer } from "./validate-server"; import { SecurityAudit } from "./security-audit"; +import { ValidateServer } from "./validate-server"; interface Props { serverId: string; diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx index d476fb15f..d45a3b77d 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -23,17 +23,17 @@ import { api } from "@/utils/api"; import { format } from "date-fns"; import { KeyIcon, MoreHorizontal, ServerIcon } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/router"; import { toast } from "sonner"; import { TerminalModal } from "../web-server/terminal-modal"; import { ShowServerActions } from "./actions/show-server-actions"; import { AddServer } from "./add-server"; import { SetupServer } from "./setup-server"; import { ShowDockerContainersModal } from "./show-docker-containers-modal"; +import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { UpdateServer } from "./update-server"; -import { useRouter } from "next/router"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; -import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; export const ShowServers = () => { const router = useRouter(); diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx index 740f79607..37f8e0176 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/create-ssh-key.tsx @@ -1,12 +1,12 @@ +import { CodeEditor } from "@/components/shared/code-editor"; import { Card, CardContent } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { ExternalLinkIcon, Loader2 } from "lucide-react"; import copy from "copy-to-clipboard"; +import { ExternalLinkIcon, Loader2 } from "lucide-react"; import { CopyIcon } from "lucide-react"; +import Link from "next/link"; import { useEffect, useRef } from "react"; import { toast } from "sonner"; -import { CodeEditor } from "@/components/shared/code-editor"; -import Link from "next/link"; export const CreateSSHKey = () => { const { data, refetch } = api.sshKey.all.useQuery(); diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/setup.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/setup.tsx index 39179de83..be7a8e48b 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/setup.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/setup.tsx @@ -5,26 +5,26 @@ import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Button } from "@/components/ui/button"; import { Card, + CardContent, + CardDescription, CardHeader, CardTitle, - CardDescription, - CardContent, } from "@/components/ui/card"; -import { RocketIcon } from "lucide-react"; -import { toast } from "sonner"; -import { EditScript } from "../edit-script"; -import { api } from "@/utils/api"; -import { useState } from "react"; import { Label } from "@/components/ui/label"; import { Select, - SelectTrigger, - SelectValue, SelectContent, SelectGroup, SelectItem, SelectLabel, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; +import { api } from "@/utils/api"; +import { RocketIcon } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { EditScript } from "../edit-script"; export const Setup = () => { const { data: servers } = api.server.all.useQuery(); diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx index 3d6cfa7df..fe8c36c2c 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/verify.tsx @@ -1,27 +1,27 @@ import { Button } from "@/components/ui/button"; import { Card, + CardContent, + CardDescription, CardHeader, CardTitle, - CardDescription, - CardContent, } from "@/components/ui/card"; -import { Loader2, PcCase, RefreshCw } from "lucide-react"; +import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; +import { Loader2, PcCase, RefreshCw } from "lucide-react"; import { useState } from "react"; -import { Label } from "@/components/ui/label"; +import { AlertBlock } from "@/components/shared/alert-block"; import { Select, - SelectTrigger, - SelectValue, SelectContent, SelectGroup, SelectItem, SelectLabel, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { StatusRow } from "../gpu-support"; -import { AlertBlock } from "@/components/shared/alert-block"; export const Verify = () => { const { data: servers } = api.server.all.useQuery(); diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx index e9de05232..bab930478 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx @@ -1,3 +1,5 @@ +import { GithubIcon } from "@/components/icons/data-tools-icons"; +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -7,21 +9,19 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Separator } from "@/components/ui/separator"; +import { defineStepper } from "@stepperize/react"; import { BookIcon, Puzzle } from "lucide-react"; +import { Code2, Database, GitMerge, Globe, Plug, Users } from "lucide-react"; +import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import { defineStepper } from "@stepperize/react"; import React from "react"; -import { Separator } from "@/components/ui/separator"; -import { AlertBlock } from "@/components/shared/alert-block"; +import ConfettiExplosion from "react-confetti-explosion"; import { CreateServer } from "./create-server"; import { CreateSSHKey } from "./create-ssh-key"; import { Setup } from "./setup"; import { Verify } from "./verify"; -import { Database, Globe, GitMerge, Users, Code2, Plug } from "lucide-react"; -import ConfettiExplosion from "react-confetti-explosion"; -import Link from "next/link"; -import { GithubIcon } from "@/components/icons/data-tools-icons"; export const { useStepper, steps, Scoped } = defineStepper( { diff --git a/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx index f5b6b2a9e..fa9f1a41d 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/docker-terminal-modal.tsx @@ -80,7 +80,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => { return ( {children} - event.preventDefault()} > diff --git a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx index 965948ca0..180b2fcbb 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -6,6 +6,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; import { Select, SelectContent, @@ -13,7 +14,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; import { useTranslation } from "next-i18next"; import type React from "react"; diff --git a/apps/dokploy/pages/api/deploy/github.ts b/apps/dokploy/pages/api/deploy/github.ts index 0e0a6c829..459237620 100644 --- a/apps/dokploy/pages/api/deploy/github.ts +++ b/apps/dokploy/pages/api/deploy/github.ts @@ -3,19 +3,19 @@ import { applications, compose, github } from "@/server/db/schema"; import type { DeploymentJob } from "@/server/queues/queue-types"; import { myQueue } from "@/server/queues/queueSetup"; import { deploy } from "@/server/utils/deploy"; +import { generateRandomDomain } from "@/templates/utils"; import { - createPreviewDeployment, type Domain, + IS_CLOUD, + createPreviewDeployment, findPreviewDeploymentByApplicationId, findPreviewDeploymentsByPullRequestId, - IS_CLOUD, removePreviewDeployment, } from "@dokploy/server"; import { Webhooks } from "@octokit/webhooks"; import { and, eq } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; import { extractCommitMessage, extractHash } from "./[refreshToken]"; -import { generateRandomDomain } from "@/templates/utils"; export default async function handler( req: NextApiRequest, diff --git a/apps/dokploy/public/locales/it/common.json b/apps/dokploy/public/locales/it/common.json index 9e26dfeeb..0967ef424 100644 --- a/apps/dokploy/public/locales/it/common.json +++ b/apps/dokploy/public/locales/it/common.json @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/apps/dokploy/public/locales/it/settings.json b/apps/dokploy/public/locales/it/settings.json index f4c2cc068..6280e44eb 100644 --- a/apps/dokploy/public/locales/it/settings.json +++ b/apps/dokploy/public/locales/it/settings.json @@ -1,44 +1,44 @@ -{ - "settings.common.save": "Salva", - "settings.server.domain.title": "Dominio del server", - "settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.", - "settings.server.domain.form.domain": "Dominio", - "settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt", - "settings.server.domain.form.certificate.label": "Certificato", - "settings.server.domain.form.certificate.placeholder": "Seleziona un certificato", - "settings.server.domain.form.certificateOptions.none": "Nessuno", - "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)", - - "settings.server.webServer.title": "Server Web", - "settings.server.webServer.description": "Ricarica o pulisci il server web.", - "settings.server.webServer.actions": "Azioni", - "settings.server.webServer.reload": "Ricarica", - "settings.server.webServer.watchLogs": "Guarda i log", - "settings.server.webServer.updateServerIp": "Aggiorna IP del server", - "settings.server.webServer.server.label": "Server", - "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Modifica Env", - "settings.server.webServer.storage.label": "Spazio", - "settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate", - "settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati", - "settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati", - "settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema", - "settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio", - "settings.server.webServer.storage.cleanAll": "Pulisci tutto", - - "settings.profile.title": "Account", - "settings.profile.description": "Modifica i dettagli del tuo profilo qui.", - "settings.profile.email": "Email", - "settings.profile.password": "Password", - "settings.profile.avatar": "Avatar", - - "settings.appearance.title": "Aspetto", - "settings.appearance.description": "Personalizza il tema della tua dashboard.", - "settings.appearance.theme": "Tema", - "settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard", - "settings.appearance.themes.light": "Chiaro", - "settings.appearance.themes.dark": "Scuro", - "settings.appearance.themes.system": "Sistema", - "settings.appearance.language": "Lingua", - "settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard" -} \ No newline at end of file +{ + "settings.common.save": "Salva", + "settings.server.domain.title": "Dominio del server", + "settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.", + "settings.server.domain.form.domain": "Dominio", + "settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt", + "settings.server.domain.form.certificate.label": "Certificato", + "settings.server.domain.form.certificate.placeholder": "Seleziona un certificato", + "settings.server.domain.form.certificateOptions.none": "Nessuno", + "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)", + + "settings.server.webServer.title": "Server Web", + "settings.server.webServer.description": "Ricarica o pulisci il server web.", + "settings.server.webServer.actions": "Azioni", + "settings.server.webServer.reload": "Ricarica", + "settings.server.webServer.watchLogs": "Guarda i log", + "settings.server.webServer.updateServerIp": "Aggiorna IP del server", + "settings.server.webServer.server.label": "Server", + "settings.server.webServer.traefik.label": "Traefik", + "settings.server.webServer.traefik.modifyEnv": "Modifica Env", + "settings.server.webServer.storage.label": "Spazio", + "settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate", + "settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati", + "settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati", + "settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema", + "settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio", + "settings.server.webServer.storage.cleanAll": "Pulisci tutto", + + "settings.profile.title": "Account", + "settings.profile.description": "Modifica i dettagli del tuo profilo qui.", + "settings.profile.email": "Email", + "settings.profile.password": "Password", + "settings.profile.avatar": "Avatar", + + "settings.appearance.title": "Aspetto", + "settings.appearance.description": "Personalizza il tema della tua dashboard.", + "settings.appearance.theme": "Tema", + "settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard", + "settings.appearance.themes.light": "Chiaro", + "settings.appearance.themes.dark": "Scuro", + "settings.appearance.themes.system": "Sistema", + "settings.appearance.language": "Lingua", + "settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard" +} diff --git a/apps/dokploy/templates/onedev/index.ts b/apps/dokploy/templates/onedev/index.ts index 5dad17282..8017c3514 100644 --- a/apps/dokploy/templates/onedev/index.ts +++ b/apps/dokploy/templates/onedev/index.ts @@ -1,22 +1,22 @@ import { - type DomainSchema, - type Schema, - type Template, - generateRandomDomain, + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, } from "../utils"; export function generate(schema: Schema): Template { - const randomDomain = generateRandomDomain(schema); + const randomDomain = generateRandomDomain(schema); - const domains: DomainSchema[] = [ - { - host: randomDomain, - port: 6610, - serviceName: "onedev", - }, - ]; + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 6610, + serviceName: "onedev", + }, + ]; - return { - domains, - }; + return { + domains, + }; } diff --git a/apps/dokploy/templates/unsend/index.ts b/apps/dokploy/templates/unsend/index.ts index a383b771f..1c4c9c715 100644 --- a/apps/dokploy/templates/unsend/index.ts +++ b/apps/dokploy/templates/unsend/index.ts @@ -1,44 +1,44 @@ import { - generateHash, - generateRandomDomain, - generateBase64, - type Template, - type Schema, - type DomainSchema, + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateHash, + generateRandomDomain, } from "../utils"; export function generate(schema: Schema): Template { - const mainDomain = generateRandomDomain(schema); - const secretBase = generateBase64(64); + const mainDomain = generateRandomDomain(schema); + const secretBase = generateBase64(64); - const domains: DomainSchema[] = [ - { - host: mainDomain, - port: 3000, - serviceName: "unsend", - }, - ]; + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "unsend", + }, + ]; - const envs = [ - "REDIS_URL=redis://unsend-redis-prod:6379", - "POSTGRES_USER=postgres", - "POSTGRES_PASSWORD=postgres", - "POSTGRES_DB=unsend", - "DATABASE_URL=postgresql://postgres:postgres@unsend-db-prod:5432/unsend", - "NEXTAUTH_URL=http://localhost:3000", - `NEXTAUTH_SECRET=${secretBase}`, - "GITHUB_ID='Fill'", - "GITHUB_SECRET='Fill'", - "AWS_DEFAULT_REGION=us-east-1", - "AWS_SECRET_KEY='Fill'", - "AWS_ACCESS_KEY='Fill'", - "DOCKER_OUTPUT=1", - "API_RATE_LIMIT=1", - "DISCORD_WEBHOOK_URL=", - ]; + const envs = [ + "REDIS_URL=redis://unsend-redis-prod:6379", + "POSTGRES_USER=postgres", + "POSTGRES_PASSWORD=postgres", + "POSTGRES_DB=unsend", + "DATABASE_URL=postgresql://postgres:postgres@unsend-db-prod:5432/unsend", + "NEXTAUTH_URL=http://localhost:3000", + `NEXTAUTH_SECRET=${secretBase}`, + "GITHUB_ID='Fill'", + "GITHUB_SECRET='Fill'", + "AWS_DEFAULT_REGION=us-east-1", + "AWS_SECRET_KEY='Fill'", + "AWS_ACCESS_KEY='Fill'", + "DOCKER_OUTPUT=1", + "API_RATE_LIMIT=1", + "DISCORD_WEBHOOK_URL=", + ]; - return { - envs, - domains, - }; + return { + envs, + domains, + }; } diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index 425f8d134..0f6aaed31 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -1,11 +1,11 @@ import { relations } from "drizzle-orm"; import { - boolean, - integer, - json, - pgEnum, - pgTable, - text, + boolean, + integer, + json, + pgEnum, + pgTable, + text, } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; @@ -28,493 +28,493 @@ import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; export const sourceType = pgEnum("sourceType", [ - "docker", - "git", - "github", - "gitlab", - "bitbucket", - "drop", + "docker", + "git", + "github", + "gitlab", + "bitbucket", + "drop", ]); export const buildType = pgEnum("buildType", [ - "dockerfile", - "heroku_buildpacks", - "paketo_buildpacks", - "nixpacks", - "static", + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", ]); // TODO: refactor this types export interface HealthCheckSwarm { - Test?: string[] | undefined; - Interval?: number | undefined; - Timeout?: number | undefined; - StartPeriod?: number | undefined; - Retries?: number | undefined; + Test?: string[] | undefined; + Interval?: number | undefined; + Timeout?: number | undefined; + StartPeriod?: number | undefined; + Retries?: number | undefined; } export interface RestartPolicySwarm { - Condition?: string | undefined; - Delay?: number | undefined; - MaxAttempts?: number | undefined; - Window?: number | undefined; + Condition?: string | undefined; + Delay?: number | undefined; + MaxAttempts?: number | undefined; + Window?: number | undefined; } export interface PlacementSwarm { - Constraints?: string[] | undefined; - Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined; - MaxReplicas?: number | undefined; - Platforms?: - | Array<{ - Architecture: string; - OS: string; - }> - | undefined; + Constraints?: string[] | undefined; + Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined; + MaxReplicas?: number | undefined; + Platforms?: + | Array<{ + Architecture: string; + OS: string; + }> + | undefined; } export interface UpdateConfigSwarm { - Parallelism: number; - Delay?: number | undefined; - FailureAction?: string | undefined; - Monitor?: number | undefined; - MaxFailureRatio?: number | undefined; - Order: string; + Parallelism: number; + Delay?: number | undefined; + FailureAction?: string | undefined; + Monitor?: number | undefined; + MaxFailureRatio?: number | undefined; + Order: string; } export interface ServiceModeSwarm { - Replicated?: { Replicas?: number | undefined } | undefined; - Global?: {} | undefined; - ReplicatedJob?: - | { - MaxConcurrent?: number | undefined; - TotalCompletions?: number | undefined; - } - | undefined; - GlobalJob?: {} | undefined; + Replicated?: { Replicas?: number | undefined } | undefined; + Global?: {} | undefined; + ReplicatedJob?: + | { + MaxConcurrent?: number | undefined; + TotalCompletions?: number | undefined; + } + | undefined; + GlobalJob?: {} | undefined; } export interface NetworkSwarm { - Target?: string | undefined; - Aliases?: string[] | undefined; - DriverOpts?: { [key: string]: string } | undefined; + Target?: string | undefined; + Aliases?: string[] | undefined; + DriverOpts?: { [key: string]: string } | undefined; } export interface LabelsSwarm { - [name: string]: string; + [name: string]: string; } export const applications = pgTable("application", { - applicationId: text("applicationId") - .notNull() - .primaryKey() - .$defaultFn(() => nanoid()), - name: text("name").notNull(), - appName: text("appName") - .notNull() - .$defaultFn(() => generateAppName("app")) - .unique(), - description: text("description"), - env: text("env"), - previewEnv: text("previewEnv"), - previewBuildArgs: text("previewBuildArgs"), - previewWildcard: text("previewWildcard"), - previewPort: integer("previewPort").default(3000), - previewHttps: boolean("previewHttps").notNull().default(false), - previewPath: text("previewPath").default("/"), - previewCertificateType: certificateType("certificateType") - .notNull() - .default("none"), - previewLimit: integer("previewLimit").default(3), - isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( - false - ), - buildArgs: text("buildArgs"), - memoryReservation: integer("memoryReservation"), - memoryLimit: integer("memoryLimit"), - cpuReservation: integer("cpuReservation"), - cpuLimit: integer("cpuLimit"), - title: text("title"), - enabled: boolean("enabled"), - subtitle: text("subtitle"), - command: text("command"), - refreshToken: text("refreshToken").$defaultFn(() => nanoid()), - sourceType: sourceType("sourceType").notNull().default("github"), - // Github - repository: text("repository"), - owner: text("owner"), - branch: text("branch"), - buildPath: text("buildPath").default("/"), - autoDeploy: boolean("autoDeploy").$defaultFn(() => true), - // Gitlab - gitlabProjectId: integer("gitlabProjectId"), - gitlabRepository: text("gitlabRepository"), - gitlabOwner: text("gitlabOwner"), - gitlabBranch: text("gitlabBranch"), - gitlabBuildPath: text("gitlabBuildPath").default("/"), - gitlabPathNamespace: text("gitlabPathNamespace"), - // Bitbucket - bitbucketRepository: text("bitbucketRepository"), - bitbucketOwner: text("bitbucketOwner"), - bitbucketBranch: text("bitbucketBranch"), - bitbucketBuildPath: text("bitbucketBuildPath").default("/"), - // Docker - username: text("username"), - password: text("password"), - dockerImage: text("dockerImage"), - registryUrl: text("registryUrl"), - // Git - customGitUrl: text("customGitUrl"), - customGitBranch: text("customGitBranch"), - customGitBuildPath: text("customGitBuildPath"), - customGitSSHKeyId: text("customGitSSHKeyId").references( - () => sshKeys.sshKeyId, - { - onDelete: "set null", - } - ), - dockerfile: text("dockerfile"), - dockerContextPath: text("dockerContextPath"), - dockerBuildStage: text("dockerBuildStage"), - // Drop - dropBuildPath: text("dropBuildPath"), - // Docker swarm json - healthCheckSwarm: json("healthCheckSwarm").$type(), - restartPolicySwarm: json("restartPolicySwarm").$type(), - placementSwarm: json("placementSwarm").$type(), - updateConfigSwarm: json("updateConfigSwarm").$type(), - rollbackConfigSwarm: json("rollbackConfigSwarm").$type(), - modeSwarm: json("modeSwarm").$type(), - labelsSwarm: json("labelsSwarm").$type(), - networkSwarm: json("networkSwarm").$type(), - // - replicas: integer("replicas").default(1).notNull(), - applicationStatus: applicationStatus("applicationStatus") - .notNull() - .default("idle"), - buildType: buildType("buildType").notNull().default("nixpacks"), - herokuVersion: text("herokuVersion").default("24"), - publishDirectory: text("publishDirectory"), - createdAt: text("createdAt") - .notNull() - .$defaultFn(() => new Date().toISOString()), - registryId: text("registryId").references(() => registry.registryId, { - onDelete: "set null", - }), - projectId: text("projectId") - .notNull() - .references(() => projects.projectId, { onDelete: "cascade" }), - githubId: text("githubId").references(() => github.githubId, { - onDelete: "set null", - }), - gitlabId: text("gitlabId").references(() => gitlab.gitlabId, { - onDelete: "set null", - }), - bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, { - onDelete: "set null", - }), - serverId: text("serverId").references(() => server.serverId, { - onDelete: "cascade", - }), + applicationId: text("applicationId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + appName: text("appName") + .notNull() + .$defaultFn(() => generateAppName("app")) + .unique(), + description: text("description"), + env: text("env"), + previewEnv: text("previewEnv"), + previewBuildArgs: text("previewBuildArgs"), + previewWildcard: text("previewWildcard"), + previewPort: integer("previewPort").default(3000), + previewHttps: boolean("previewHttps").notNull().default(false), + previewPath: text("previewPath").default("/"), + previewCertificateType: certificateType("certificateType") + .notNull() + .default("none"), + previewLimit: integer("previewLimit").default(3), + isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( + false, + ), + buildArgs: text("buildArgs"), + memoryReservation: integer("memoryReservation"), + memoryLimit: integer("memoryLimit"), + cpuReservation: integer("cpuReservation"), + cpuLimit: integer("cpuLimit"), + title: text("title"), + enabled: boolean("enabled"), + subtitle: text("subtitle"), + command: text("command"), + refreshToken: text("refreshToken").$defaultFn(() => nanoid()), + sourceType: sourceType("sourceType").notNull().default("github"), + // Github + repository: text("repository"), + owner: text("owner"), + branch: text("branch"), + buildPath: text("buildPath").default("/"), + autoDeploy: boolean("autoDeploy").$defaultFn(() => true), + // Gitlab + gitlabProjectId: integer("gitlabProjectId"), + gitlabRepository: text("gitlabRepository"), + gitlabOwner: text("gitlabOwner"), + gitlabBranch: text("gitlabBranch"), + gitlabBuildPath: text("gitlabBuildPath").default("/"), + gitlabPathNamespace: text("gitlabPathNamespace"), + // Bitbucket + bitbucketRepository: text("bitbucketRepository"), + bitbucketOwner: text("bitbucketOwner"), + bitbucketBranch: text("bitbucketBranch"), + bitbucketBuildPath: text("bitbucketBuildPath").default("/"), + // Docker + username: text("username"), + password: text("password"), + dockerImage: text("dockerImage"), + registryUrl: text("registryUrl"), + // Git + customGitUrl: text("customGitUrl"), + customGitBranch: text("customGitBranch"), + customGitBuildPath: text("customGitBuildPath"), + customGitSSHKeyId: text("customGitSSHKeyId").references( + () => sshKeys.sshKeyId, + { + onDelete: "set null", + }, + ), + dockerfile: text("dockerfile"), + dockerContextPath: text("dockerContextPath"), + dockerBuildStage: text("dockerBuildStage"), + // Drop + dropBuildPath: text("dropBuildPath"), + // Docker swarm json + healthCheckSwarm: json("healthCheckSwarm").$type(), + restartPolicySwarm: json("restartPolicySwarm").$type(), + placementSwarm: json("placementSwarm").$type(), + updateConfigSwarm: json("updateConfigSwarm").$type(), + rollbackConfigSwarm: json("rollbackConfigSwarm").$type(), + modeSwarm: json("modeSwarm").$type(), + labelsSwarm: json("labelsSwarm").$type(), + networkSwarm: json("networkSwarm").$type(), + // + replicas: integer("replicas").default(1).notNull(), + applicationStatus: applicationStatus("applicationStatus") + .notNull() + .default("idle"), + buildType: buildType("buildType").notNull().default("nixpacks"), + herokuVersion: text("herokuVersion").default("24"), + publishDirectory: text("publishDirectory"), + createdAt: text("createdAt") + .notNull() + .$defaultFn(() => new Date().toISOString()), + registryId: text("registryId").references(() => registry.registryId, { + onDelete: "set null", + }), + projectId: text("projectId") + .notNull() + .references(() => projects.projectId, { onDelete: "cascade" }), + githubId: text("githubId").references(() => github.githubId, { + onDelete: "set null", + }), + gitlabId: text("gitlabId").references(() => gitlab.gitlabId, { + onDelete: "set null", + }), + bitbucketId: text("bitbucketId").references(() => bitbucket.bitbucketId, { + onDelete: "set null", + }), + serverId: text("serverId").references(() => server.serverId, { + onDelete: "cascade", + }), }); export const applicationsRelations = relations( - applications, - ({ one, many }) => ({ - project: one(projects, { - fields: [applications.projectId], - references: [projects.projectId], - }), - deployments: many(deployments), - customGitSSHKey: one(sshKeys, { - fields: [applications.customGitSSHKeyId], - references: [sshKeys.sshKeyId], - }), - domains: many(domains), - mounts: many(mounts), - redirects: many(redirects), - security: many(security), - ports: many(ports), - registry: one(registry, { - fields: [applications.registryId], - references: [registry.registryId], - }), - github: one(github, { - fields: [applications.githubId], - references: [github.githubId], - }), - gitlab: one(gitlab, { - fields: [applications.gitlabId], - references: [gitlab.gitlabId], - }), - bitbucket: one(bitbucket, { - fields: [applications.bitbucketId], - references: [bitbucket.bitbucketId], - }), - server: one(server, { - fields: [applications.serverId], - references: [server.serverId], - }), - previewDeployments: many(previewDeployments), - }) + applications, + ({ one, many }) => ({ + project: one(projects, { + fields: [applications.projectId], + references: [projects.projectId], + }), + deployments: many(deployments), + customGitSSHKey: one(sshKeys, { + fields: [applications.customGitSSHKeyId], + references: [sshKeys.sshKeyId], + }), + domains: many(domains), + mounts: many(mounts), + redirects: many(redirects), + security: many(security), + ports: many(ports), + registry: one(registry, { + fields: [applications.registryId], + references: [registry.registryId], + }), + github: one(github, { + fields: [applications.githubId], + references: [github.githubId], + }), + gitlab: one(gitlab, { + fields: [applications.gitlabId], + references: [gitlab.gitlabId], + }), + bitbucket: one(bitbucket, { + fields: [applications.bitbucketId], + references: [bitbucket.bitbucketId], + }), + server: one(server, { + fields: [applications.serverId], + references: [server.serverId], + }), + previewDeployments: many(previewDeployments), + }), ); const HealthCheckSwarmSchema = z - .object({ - Test: z.array(z.string()).optional(), - Interval: z.number().optional(), - Timeout: z.number().optional(), - StartPeriod: z.number().optional(), - Retries: z.number().optional(), - }) - .strict(); + .object({ + Test: z.array(z.string()).optional(), + Interval: z.number().optional(), + Timeout: z.number().optional(), + StartPeriod: z.number().optional(), + Retries: z.number().optional(), + }) + .strict(); const RestartPolicySwarmSchema = z - .object({ - Condition: z.string().optional(), - Delay: z.number().optional(), - MaxAttempts: z.number().optional(), - Window: z.number().optional(), - }) - .strict(); + .object({ + Condition: z.string().optional(), + Delay: z.number().optional(), + MaxAttempts: z.number().optional(), + Window: z.number().optional(), + }) + .strict(); const PreferenceSchema = z - .object({ - Spread: z.object({ - SpreadDescriptor: z.string(), - }), - }) - .strict(); + .object({ + Spread: z.object({ + SpreadDescriptor: z.string(), + }), + }) + .strict(); const PlatformSchema = z - .object({ - Architecture: z.string(), - OS: z.string(), - }) - .strict(); + .object({ + Architecture: z.string(), + OS: z.string(), + }) + .strict(); const PlacementSwarmSchema = z - .object({ - Constraints: z.array(z.string()).optional(), - Preferences: z.array(PreferenceSchema).optional(), - MaxReplicas: z.number().optional(), - Platforms: z.array(PlatformSchema).optional(), - }) - .strict(); + .object({ + Constraints: z.array(z.string()).optional(), + Preferences: z.array(PreferenceSchema).optional(), + MaxReplicas: z.number().optional(), + Platforms: z.array(PlatformSchema).optional(), + }) + .strict(); const UpdateConfigSwarmSchema = z - .object({ - Parallelism: z.number(), - Delay: z.number().optional(), - FailureAction: z.string().optional(), - Monitor: z.number().optional(), - MaxFailureRatio: z.number().optional(), - Order: z.string(), - }) - .strict(); + .object({ + Parallelism: z.number(), + Delay: z.number().optional(), + FailureAction: z.string().optional(), + Monitor: z.number().optional(), + MaxFailureRatio: z.number().optional(), + Order: z.string(), + }) + .strict(); const ReplicatedSchema = z - .object({ - Replicas: z.number().optional(), - }) - .strict(); + .object({ + Replicas: z.number().optional(), + }) + .strict(); const ReplicatedJobSchema = z - .object({ - MaxConcurrent: z.number().optional(), - TotalCompletions: z.number().optional(), - }) - .strict(); + .object({ + MaxConcurrent: z.number().optional(), + TotalCompletions: z.number().optional(), + }) + .strict(); const ServiceModeSwarmSchema = z - .object({ - Replicated: ReplicatedSchema.optional(), - Global: z.object({}).optional(), - ReplicatedJob: ReplicatedJobSchema.optional(), - GlobalJob: z.object({}).optional(), - }) - .strict(); + .object({ + Replicated: ReplicatedSchema.optional(), + Global: z.object({}).optional(), + ReplicatedJob: ReplicatedJobSchema.optional(), + GlobalJob: z.object({}).optional(), + }) + .strict(); const NetworkSwarmSchema = z.array( - z - .object({ - Target: z.string().optional(), - Aliases: z.array(z.string()).optional(), - DriverOpts: z.object({}).optional(), - }) - .strict() + z + .object({ + Target: z.string().optional(), + Aliases: z.array(z.string()).optional(), + DriverOpts: z.object({}).optional(), + }) + .strict(), ); const LabelsSwarmSchema = z.record(z.string()); const createSchema = createInsertSchema(applications, { - appName: z.string(), - createdAt: z.string(), - applicationId: z.string(), - autoDeploy: z.boolean(), - env: z.string().optional(), - buildArgs: z.string().optional(), - name: z.string().min(1), - description: z.string().optional(), - memoryReservation: z.number().optional(), - memoryLimit: z.number().optional(), - cpuReservation: z.number().optional(), - cpuLimit: z.number().optional(), - title: z.string().optional(), - enabled: z.boolean().optional(), - subtitle: z.string().optional(), - dockerImage: z.string().optional(), - username: z.string().optional(), - isPreviewDeploymentsActive: z.boolean().optional(), - password: z.string().optional(), - registryUrl: z.string().optional(), - customGitSSHKeyId: z.string().optional(), - repository: z.string().optional(), - dockerfile: z.string().optional(), - branch: z.string().optional(), - customGitBranch: z.string().optional(), - customGitBuildPath: z.string().optional(), - customGitUrl: z.string().optional(), - buildPath: z.string().optional(), - projectId: z.string(), - sourceType: z.enum(["github", "docker", "git"]).optional(), - applicationStatus: z.enum(["idle", "running", "done", "error"]), - buildType: z.enum([ - "dockerfile", - "heroku_buildpacks", - "paketo_buildpacks", - "nixpacks", - "static", - ]), - herokuVersion: z.string().optional(), - publishDirectory: z.string().optional(), - owner: z.string(), - healthCheckSwarm: HealthCheckSwarmSchema.nullable(), - restartPolicySwarm: RestartPolicySwarmSchema.nullable(), - placementSwarm: PlacementSwarmSchema.nullable(), - updateConfigSwarm: UpdateConfigSwarmSchema.nullable(), - rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(), - modeSwarm: ServiceModeSwarmSchema.nullable(), - labelsSwarm: LabelsSwarmSchema.nullable(), - networkSwarm: NetworkSwarmSchema.nullable(), - previewPort: z.number().optional(), - previewEnv: z.string().optional(), - previewBuildArgs: z.string().optional(), - previewWildcard: z.string().optional(), - previewLimit: z.number().optional(), - previewHttps: z.boolean().optional(), - previewPath: z.string().optional(), - previewCertificateType: z.enum(["letsencrypt", "none"]).optional(), + appName: z.string(), + createdAt: z.string(), + applicationId: z.string(), + autoDeploy: z.boolean(), + env: z.string().optional(), + buildArgs: z.string().optional(), + name: z.string().min(1), + description: z.string().optional(), + memoryReservation: z.number().optional(), + memoryLimit: z.number().optional(), + cpuReservation: z.number().optional(), + cpuLimit: z.number().optional(), + title: z.string().optional(), + enabled: z.boolean().optional(), + subtitle: z.string().optional(), + dockerImage: z.string().optional(), + username: z.string().optional(), + isPreviewDeploymentsActive: z.boolean().optional(), + password: z.string().optional(), + registryUrl: z.string().optional(), + customGitSSHKeyId: z.string().optional(), + repository: z.string().optional(), + dockerfile: z.string().optional(), + branch: z.string().optional(), + customGitBranch: z.string().optional(), + customGitBuildPath: z.string().optional(), + customGitUrl: z.string().optional(), + buildPath: z.string().optional(), + projectId: z.string(), + sourceType: z.enum(["github", "docker", "git"]).optional(), + applicationStatus: z.enum(["idle", "running", "done", "error"]), + buildType: z.enum([ + "dockerfile", + "heroku_buildpacks", + "paketo_buildpacks", + "nixpacks", + "static", + ]), + herokuVersion: z.string().optional(), + publishDirectory: z.string().optional(), + owner: z.string(), + healthCheckSwarm: HealthCheckSwarmSchema.nullable(), + restartPolicySwarm: RestartPolicySwarmSchema.nullable(), + placementSwarm: PlacementSwarmSchema.nullable(), + updateConfigSwarm: UpdateConfigSwarmSchema.nullable(), + rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(), + modeSwarm: ServiceModeSwarmSchema.nullable(), + labelsSwarm: LabelsSwarmSchema.nullable(), + networkSwarm: NetworkSwarmSchema.nullable(), + previewPort: z.number().optional(), + previewEnv: z.string().optional(), + previewBuildArgs: z.string().optional(), + previewWildcard: z.string().optional(), + previewLimit: z.number().optional(), + previewHttps: z.boolean().optional(), + previewPath: z.string().optional(), + previewCertificateType: z.enum(["letsencrypt", "none"]).optional(), }); export const apiCreateApplication = createSchema.pick({ - name: true, - appName: true, - description: true, - projectId: true, - serverId: true, + name: true, + appName: true, + description: true, + projectId: true, + serverId: true, }); export const apiFindOneApplication = createSchema - .pick({ - applicationId: true, - }) - .required(); + .pick({ + applicationId: true, + }) + .required(); export const apiReloadApplication = createSchema - .pick({ - appName: true, - applicationId: true, - }) - .required(); + .pick({ + appName: true, + applicationId: true, + }) + .required(); export const apiSaveBuildType = createSchema - .pick({ - applicationId: true, - buildType: true, - dockerfile: true, - dockerContextPath: true, - dockerBuildStage: true, - herokuVersion: true, - }) - .required() - .merge(createSchema.pick({ publishDirectory: true })); + .pick({ + applicationId: true, + buildType: true, + dockerfile: true, + dockerContextPath: true, + dockerBuildStage: true, + herokuVersion: true, + }) + .required() + .merge(createSchema.pick({ publishDirectory: true })); export const apiSaveGithubProvider = createSchema - .pick({ - applicationId: true, - repository: true, - branch: true, - owner: true, - buildPath: true, - githubId: true, - }) - .required(); + .pick({ + applicationId: true, + repository: true, + branch: true, + owner: true, + buildPath: true, + githubId: true, + }) + .required(); export const apiSaveGitlabProvider = createSchema - .pick({ - applicationId: true, - gitlabBranch: true, - gitlabBuildPath: true, - gitlabOwner: true, - gitlabRepository: true, - gitlabId: true, - gitlabProjectId: true, - gitlabPathNamespace: true, - }) - .required(); + .pick({ + applicationId: true, + gitlabBranch: true, + gitlabBuildPath: true, + gitlabOwner: true, + gitlabRepository: true, + gitlabId: true, + gitlabProjectId: true, + gitlabPathNamespace: true, + }) + .required(); export const apiSaveBitbucketProvider = createSchema - .pick({ - bitbucketBranch: true, - bitbucketBuildPath: true, - bitbucketOwner: true, - bitbucketRepository: true, - bitbucketId: true, - applicationId: true, - }) - .required(); + .pick({ + bitbucketBranch: true, + bitbucketBuildPath: true, + bitbucketOwner: true, + bitbucketRepository: true, + bitbucketId: true, + applicationId: true, + }) + .required(); export const apiSaveDockerProvider = createSchema - .pick({ - dockerImage: true, - applicationId: true, - username: true, - password: true, - registryUrl: true, - }) - .required(); + .pick({ + dockerImage: true, + applicationId: true, + username: true, + password: true, + registryUrl: true, + }) + .required(); export const apiSaveGitProvider = createSchema - .pick({ - customGitBranch: true, - applicationId: true, - customGitBuildPath: true, - customGitUrl: true, - }) - .required() - .merge( - createSchema.pick({ - customGitSSHKeyId: true, - }) - ); + .pick({ + customGitBranch: true, + applicationId: true, + customGitBuildPath: true, + customGitUrl: true, + }) + .required() + .merge( + createSchema.pick({ + customGitSSHKeyId: true, + }), + ); export const apiSaveEnvironmentVariables = createSchema - .pick({ - applicationId: true, - env: true, - buildArgs: true, - }) - .required(); + .pick({ + applicationId: true, + env: true, + buildArgs: true, + }) + .required(); export const apiFindMonitoringStats = createSchema - .pick({ - appName: true, - }) - .required(); + .pick({ + appName: true, + }) + .required(); export const apiUpdateApplication = createSchema - .partial() - .extend({ - applicationId: z.string().min(1), - }) - .omit({ serverId: true }); + .partial() + .extend({ + applicationId: z.string().min(1), + }) + .omit({ serverId: true }); diff --git a/packages/server/src/db/schema/deployment.ts b/packages/server/src/db/schema/deployment.ts index f79b48ee9..ccaf64661 100644 --- a/packages/server/src/db/schema/deployment.ts +++ b/packages/server/src/db/schema/deployment.ts @@ -11,8 +11,8 @@ import { nanoid } from "nanoid"; import { z } from "zod"; import { applications } from "./application"; import { compose } from "./compose"; -import { server } from "./server"; import { previewDeployments } from "./preview-deployments"; +import { server } from "./server"; export const deploymentStatus = pgEnum("deploymentStatus", [ "running", diff --git a/packages/server/src/db/schema/domain.ts b/packages/server/src/db/schema/domain.ts index f115ce667..5c34d455d 100644 --- a/packages/server/src/db/schema/domain.ts +++ b/packages/server/src/db/schema/domain.ts @@ -14,8 +14,8 @@ import { z } from "zod"; import { domain } from "../validations/domain"; import { applications } from "./application"; import { compose } from "./compose"; -import { certificateType } from "./shared"; import { previewDeployments } from "./preview-deployments"; +import { certificateType } from "./shared"; export const domainType = pgEnum("domainType", [ "compose", diff --git a/packages/server/src/db/schema/index.ts b/packages/server/src/db/schema/index.ts index f07a18707..9c7a079c5 100644 --- a/packages/server/src/db/schema/index.ts +++ b/packages/server/src/db/schema/index.ts @@ -29,4 +29,4 @@ export * from "./github"; export * from "./gitlab"; export * from "./server"; export * from "./utils"; -export * from "./preview-deployments"; \ No newline at end of file +export * from "./preview-deployments"; diff --git a/packages/server/src/db/schema/preview-deployments.ts b/packages/server/src/db/schema/preview-deployments.ts index 5d0671e8d..3bdab2c25 100644 --- a/packages/server/src/db/schema/preview-deployments.ts +++ b/packages/server/src/db/schema/preview-deployments.ts @@ -1,13 +1,13 @@ import { relations } from "drizzle-orm"; import { pgTable, text } from "drizzle-orm/pg-core"; +import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; +import { z } from "zod"; import { applications } from "./application"; -import { domains } from "./domain"; import { deployments } from "./deployment"; -import { createInsertSchema } from "drizzle-zod"; -import { z } from "zod"; -import { generateAppName } from "./utils"; +import { domains } from "./domain"; import { applicationStatus } from "./shared"; +import { generateAppName } from "./utils"; export const previewDeployments = pgTable("preview_deployments", { previewDeploymentId: text("previewDeploymentId") diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index 797feb383..6fec7a319 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -7,12 +7,12 @@ import { cleanUpSystemPrune, cleanUpUnusedImages, } from "../docker/utils"; +import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; +import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; import { runMariadbBackup } from "./mariadb"; import { runMongoBackup } from "./mongo"; import { runMySqlBackup } from "./mysql"; import { runPostgresBackup } from "./postgres"; -import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; -import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index ce10413ac..df85d98f9 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -2,6 +2,7 @@ import { createWriteStream } from "node:fs"; import { join } from "node:path"; import type { InferResultType } from "@dokploy/server/types/with"; import type { CreateServiceOptions } from "dockerode"; +import { nanoid } from "nanoid"; import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload"; import { calculateResources, @@ -17,7 +18,6 @@ import { buildHeroku, getHerokuCommand } from "./heroku"; import { buildNixpacks, getNixpacksCommand } from "./nixpacks"; import { buildPaketo, getPaketoCommand } from "./paketo"; import { buildStatic, getStaticCommand } from "./static"; -import { nanoid } from "nanoid"; // NIXPACKS codeDirectory = where is the path of the code directory // HEROKU codeDirectory = where is the path of the code directory diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index e8c9e6c23..1c4a38ab4 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -15,528 +15,528 @@ import { spawnAsync } from "../process/spawnAsync"; import { getRemoteDocker } from "../servers/remote-docker"; interface RegistryAuth { - username: string; - password: string; - registryUrl: string; + username: string; + password: string; + registryUrl: string; } export const pullImage = async ( - dockerImage: string, - onData?: (data: any) => void, - authConfig?: Partial + dockerImage: string, + onData?: (data: any) => void, + authConfig?: Partial, ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } - - if (authConfig?.username && authConfig?.password) { - await spawnAsync( - "docker", - [ - "login", - authConfig.registryUrl || "", - "-u", - authConfig.username, - "-p", - authConfig.password, - ], - onData - ); - } - await spawnAsync("docker", ["pull", dockerImage], onData); - } catch (error) { - throw error; - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } + + if (authConfig?.username && authConfig?.password) { + await spawnAsync( + "docker", + [ + "login", + authConfig.registryUrl || "", + "-u", + authConfig.username, + "-p", + authConfig.password, + ], + onData, + ); + } + await spawnAsync("docker", ["pull", dockerImage], onData); + } catch (error) { + throw error; + } }; export const pullRemoteImage = async ( - dockerImage: string, - serverId: string, - onData?: (data: any) => void, - authConfig?: Partial + dockerImage: string, + serverId: string, + onData?: (data: any) => void, + authConfig?: Partial, ): Promise => { - try { - if (!dockerImage) { - throw new Error("Docker image not found"); - } - - const remoteDocker = await getRemoteDocker(serverId); - - await new Promise((resolve, reject) => { - remoteDocker.pull( - dockerImage, - { authconfig: authConfig }, - (err, stream) => { - if (err) { - reject(err); - return; - } - - remoteDocker.modem.followProgress( - stream as Readable, - (err: Error | null, res) => { - if (!err) { - resolve(res); - } - if (err) { - reject(err); - } - }, - (event) => { - onData?.(event); - } - ); - } - ); - }); - } catch (error) { - throw error; - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } + + const remoteDocker = await getRemoteDocker(serverId); + + await new Promise((resolve, reject) => { + remoteDocker.pull( + dockerImage, + { authconfig: authConfig }, + (err, stream) => { + if (err) { + reject(err); + return; + } + + remoteDocker.modem.followProgress( + stream as Readable, + (err: Error | null, res) => { + if (!err) { + resolve(res); + } + if (err) { + reject(err); + } + }, + (event) => { + onData?.(event); + }, + ); + }, + ); + }); + } catch (error) { + throw error; + } }; export const containerExists = async (containerName: string) => { - const container = docker.getContainer(containerName); - try { - await container.inspect(); - return true; - } catch (error) { - return false; - } + const container = docker.getContainer(containerName); + try { + await container.inspect(); + return true; + } catch (error) { + return false; + } }; export const stopService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsync(`docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const stopServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); - } catch (error) { - console.error(error); - return error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=0 `); + } catch (error) { + console.error(error); + return error; + } }; export const getContainerByName = (name: string): Promise => { - const opts = { - limit: 1, - filters: { - name: [name], - }, - }; - return new Promise((resolve, reject) => { - docker.listContainers(opts, (err, containers) => { - if (err) { - reject(err); - } else if (containers?.length === 0) { - reject(new Error(`No container found with name: ${name}`)); - } else if (containers && containers?.length > 0 && containers[0]) { - resolve(containers[0]); - } - }); - }); + const opts = { + limit: 1, + filters: { + name: [name], + }, + }; + return new Promise((resolve, reject) => { + docker.listContainers(opts, (err, containers) => { + if (err) { + reject(err); + } else if (containers?.length === 0) { + reject(new Error(`No container found with name: ${name}`)); + } else if (containers && containers?.length > 0 && containers[0]) { + resolve(containers[0]); + } + }); + }); }; export const cleanUpUnusedImages = async (serverId?: string) => { - try { - if (serverId) { - await execAsyncRemote(serverId, "docker image prune --all --force"); - } else { - await execAsync("docker image prune --all --force"); - } - } catch (error) { - console.error(error); - throw error; - } + try { + if (serverId) { + await execAsyncRemote(serverId, "docker image prune --all --force"); + } else { + await execAsync("docker image prune --all --force"); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanStoppedContainers = async (serverId?: string) => { - try { - if (serverId) { - await execAsyncRemote(serverId, "docker container prune --force"); - } else { - await execAsync("docker container prune --force"); - } - } catch (error) { - console.error(error); - throw error; - } + try { + if (serverId) { + await execAsyncRemote(serverId, "docker container prune --force"); + } else { + await execAsync("docker container prune --force"); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpUnusedVolumes = async (serverId?: string) => { - try { - if (serverId) { - await execAsyncRemote(serverId, "docker volume prune --all --force"); - } else { - await execAsync("docker volume prune --all --force"); - } - } catch (error) { - console.error(error); - throw error; - } + try { + if (serverId) { + await execAsyncRemote(serverId, "docker volume prune --all --force"); + } else { + await execAsync("docker volume prune --all --force"); + } + } catch (error) { + console.error(error); + throw error; + } }; export const cleanUpInactiveContainers = async () => { - try { - const containers = await docker.listContainers({ all: true }); - const inactiveContainers = containers.filter( - (container) => container.State !== "running" - ); - - for (const container of inactiveContainers) { - await docker.getContainer(container.Id).remove({ force: true }); - console.log(`Cleaning up inactive container: ${container.Id}`); - } - } catch (error) { - console.error("Error cleaning up inactive containers:", error); - throw error; - } + try { + const containers = await docker.listContainers({ all: true }); + const inactiveContainers = containers.filter( + (container) => container.State !== "running", + ); + + for (const container of inactiveContainers) { + await docker.getContainer(container.Id).remove({ force: true }); + console.log(`Cleaning up inactive container: ${container.Id}`); + } + } catch (error) { + console.error("Error cleaning up inactive containers:", error); + throw error; + } }; export const cleanUpDockerBuilder = async (serverId?: string) => { - if (serverId) { - await execAsyncRemote(serverId, "docker builder prune --all --force"); - } else { - await execAsync("docker builder prune --all --force"); - } + if (serverId) { + await execAsyncRemote(serverId, "docker builder prune --all --force"); + } else { + await execAsync("docker builder prune --all --force"); + } }; export const cleanUpSystemPrune = async (serverId?: string) => { - if (serverId) { - await execAsyncRemote( - serverId, - "docker system prune --all --force --volumes" - ); - } else { - await execAsync("docker system prune --all --force --volumes"); - } + if (serverId) { + await execAsyncRemote( + serverId, + "docker system prune --all --force --volumes", + ); + } else { + await execAsync("docker system prune --all --force --volumes"); + } }; export const startService = async (appName: string) => { - try { - await execAsync(`docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsync(`docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const startServiceRemote = async (serverId: string, appName: string) => { - try { - await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); - } catch (error) { - console.error(error); - throw error; - } + try { + await execAsyncRemote(serverId, `docker service scale ${appName}=1 `); + } catch (error) { + console.error(error); + throw error; + } }; export const removeService = async ( - appName: string, - serverId?: string | null, - deleteVolumes = false + appName: string, + serverId?: string | null, + deleteVolumes = false, ) => { - try { - let command: string; - - if (deleteVolumes) { - command = `docker service rm --force ${appName}`; - } else { - command = `docker service rm ${appName}`; - } - - if (serverId) { - await execAsyncRemote(serverId, command); - } else { - await execAsync(command); - } - } catch (error) { - return error; - } + try { + let command: string; + + if (deleteVolumes) { + command = `docker service rm --force ${appName}`; + } else { + command = `docker service rm ${appName}`; + } + + if (serverId) { + await execAsyncRemote(serverId, command); + } else { + await execAsync(command); + } + } catch (error) { + return error; + } }; export const prepareEnvironmentVariables = ( - serviceEnv: string | null, - projectEnv?: string | null + serviceEnv: string | null, + projectEnv?: string | null, ) => { - const projectVars = parse(projectEnv ?? ""); - const serviceVars = parse(serviceEnv ?? ""); - - const resolvedVars = Object.entries(serviceVars).map(([key, value]) => { - let resolvedValue = value; - if (projectVars) { - resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => { - if (projectVars[ref] !== undefined) { - return projectVars[ref]; - } - throw new Error(`Invalid project environment variable: project.${ref}`); - }); - } - return `${key}=${resolvedValue}`; - }); - - return resolvedVars; + const projectVars = parse(projectEnv ?? ""); + const serviceVars = parse(serviceEnv ?? ""); + + const resolvedVars = Object.entries(serviceVars).map(([key, value]) => { + let resolvedValue = value; + if (projectVars) { + resolvedValue = value.replace(/\$\{\{project\.(.*?)\}\}/g, (_, ref) => { + if (projectVars[ref] !== undefined) { + return projectVars[ref]; + } + throw new Error(`Invalid project environment variable: project.${ref}`); + }); + } + return `${key}=${resolvedValue}`; + }); + + return resolvedVars; }; export const prepareBuildArgs = (input: string | null) => { - const pairs = (input ?? "").split("\n"); + const pairs = (input ?? "").split("\n"); - const jsonObject: Record = {}; + const jsonObject: Record = {}; - for (const pair of pairs) { - const [key, value] = pair.split("="); - if (key && value) { - jsonObject[key] = value; - } - } + for (const pair of pairs) { + const [key, value] = pair.split("="); + if (key && value) { + jsonObject[key] = value; + } + } - return jsonObject; + return jsonObject; }; export const generateVolumeMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } - - return mounts - .filter((mount) => mount.type === "volume") - .map((mount) => ({ - Type: "volume" as const, - Source: mount.volumeName || "", - Target: mount.mountPath, - })); + if (!mounts || mounts.length === 0) { + return []; + } + + return mounts + .filter((mount) => mount.type === "volume") + .map((mount) => ({ + Type: "volume" as const, + Source: mount.volumeName || "", + Target: mount.mountPath, + })); }; type Resources = { - memoryLimit: number | null; - memoryReservation: number | null; - cpuLimit: number | null; - cpuReservation: number | null; + memoryLimit: number | null; + memoryReservation: number | null; + cpuLimit: number | null; + cpuReservation: number | null; }; export const calculateResources = ({ - memoryLimit, - memoryReservation, - cpuLimit, - cpuReservation, + memoryLimit, + memoryReservation, + cpuLimit, + cpuReservation, }: Resources): ResourceRequirements => { - return { - Limits: { - MemoryBytes: memoryLimit ?? undefined, - NanoCPUs: cpuLimit ?? undefined, - }, - Reservations: { - MemoryBytes: memoryReservation ?? undefined, - NanoCPUs: cpuReservation ?? undefined, - }, - }; + return { + Limits: { + MemoryBytes: memoryLimit ?? undefined, + NanoCPUs: cpuLimit ?? undefined, + }, + Reservations: { + MemoryBytes: memoryReservation ?? undefined, + NanoCPUs: cpuReservation ?? undefined, + }, + }; }; export const generateConfigContainer = (application: ApplicationNested) => { - const { - healthCheckSwarm, - restartPolicySwarm, - placementSwarm, - updateConfigSwarm, - rollbackConfigSwarm, - modeSwarm, - labelsSwarm, - replicas, - mounts, - networkSwarm, - } = application; - - const haveMounts = mounts.length > 0; - - return { - ...(healthCheckSwarm && { - HealthCheck: healthCheckSwarm, - }), - ...(restartPolicySwarm - ? { - RestartPolicy: restartPolicySwarm, - } - : {}), - ...(placementSwarm - ? { - Placement: placementSwarm, - } - : { - // if app have mounts keep manager as constraint - Placement: { - Constraints: haveMounts ? ["node.role==manager"] : [], - }, - }), - ...(labelsSwarm && { - Labels: labelsSwarm, - }), - ...(modeSwarm - ? { - Mode: modeSwarm, - } - : { - // use replicas value if no modeSwarm provided - Mode: { - Replicated: { - Replicas: replicas, - }, - }, - }), - ...(rollbackConfigSwarm && { - RollbackConfig: rollbackConfigSwarm, - }), - ...(updateConfigSwarm - ? { UpdateConfig: updateConfigSwarm } - : { - // default config if no updateConfigSwarm provided - UpdateConfig: { - Parallelism: 1, - Order: "start-first", - }, - }), - ...(networkSwarm - ? { - Networks: networkSwarm, - } - : { - Networks: [{ Target: "dokploy-network" }], - }), - }; + const { + healthCheckSwarm, + restartPolicySwarm, + placementSwarm, + updateConfigSwarm, + rollbackConfigSwarm, + modeSwarm, + labelsSwarm, + replicas, + mounts, + networkSwarm, + } = application; + + const haveMounts = mounts.length > 0; + + return { + ...(healthCheckSwarm && { + HealthCheck: healthCheckSwarm, + }), + ...(restartPolicySwarm + ? { + RestartPolicy: restartPolicySwarm, + } + : {}), + ...(placementSwarm + ? { + Placement: placementSwarm, + } + : { + // if app have mounts keep manager as constraint + Placement: { + Constraints: haveMounts ? ["node.role==manager"] : [], + }, + }), + ...(labelsSwarm && { + Labels: labelsSwarm, + }), + ...(modeSwarm + ? { + Mode: modeSwarm, + } + : { + // use replicas value if no modeSwarm provided + Mode: { + Replicated: { + Replicas: replicas, + }, + }, + }), + ...(rollbackConfigSwarm && { + RollbackConfig: rollbackConfigSwarm, + }), + ...(updateConfigSwarm + ? { UpdateConfig: updateConfigSwarm } + : { + // default config if no updateConfigSwarm provided + UpdateConfig: { + Parallelism: 1, + Order: "start-first", + }, + }), + ...(networkSwarm + ? { + Networks: networkSwarm, + } + : { + Networks: [{ Target: "dokploy-network" }], + }), + }; }; export const generateBindMounts = (mounts: ApplicationNested["mounts"]) => { - if (!mounts || mounts.length === 0) { - return []; - } - - return mounts - .filter((mount) => mount.type === "bind") - .map((mount) => ({ - Type: "bind" as const, - Source: mount.hostPath || "", - Target: mount.mountPath, - })); + if (!mounts || mounts.length === 0) { + return []; + } + + return mounts + .filter((mount) => mount.type === "bind") + .map((mount) => ({ + Type: "bind" as const, + Source: mount.hostPath || "", + Target: mount.mountPath, + })); }; export const generateFileMounts = ( - appName: string, - service: - | ApplicationNested - | MongoNested - | MariadbNested - | MysqlNested - | PostgresNested - | RedisNested + appName: string, + service: + | ApplicationNested + | MongoNested + | MariadbNested + | MysqlNested + | PostgresNested + | RedisNested, ) => { - const { mounts } = service; - const { APPLICATIONS_PATH } = paths(!!service.serverId); - if (!mounts || mounts.length === 0) { - return []; - } - - return mounts - .filter((mount) => mount.type === "file") - .map((mount) => { - const fileName = mount.filePath; - const absoluteBasePath = path.resolve(APPLICATIONS_PATH); - const directory = path.join(absoluteBasePath, appName, "files"); - const sourcePath = path.join(directory, fileName || ""); - return { - Type: "bind" as const, - Source: sourcePath, - Target: mount.mountPath, - }; - }); + const { mounts } = service; + const { APPLICATIONS_PATH } = paths(!!service.serverId); + if (!mounts || mounts.length === 0) { + return []; + } + + return mounts + .filter((mount) => mount.type === "file") + .map((mount) => { + const fileName = mount.filePath; + const absoluteBasePath = path.resolve(APPLICATIONS_PATH); + const directory = path.join(absoluteBasePath, appName, "files"); + const sourcePath = path.join(directory, fileName || ""); + return { + Type: "bind" as const, + Source: sourcePath, + Target: mount.mountPath, + }; + }); }; export const createFile = async ( - outputPath: string, - filePath: string, - content: string + outputPath: string, + filePath: string, + content: string, ) => { - try { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - fs.mkdirSync(fullPath, { recursive: true }); - return; - } - - const directory = path.dirname(fullPath); - fs.mkdirSync(directory, { recursive: true }); - fs.writeFileSync(fullPath, content || ""); - } catch (error) { - throw error; - } + try { + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + fs.mkdirSync(fullPath, { recursive: true }); + return; + } + + const directory = path.dirname(fullPath); + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(fullPath, content || ""); + } catch (error) { + throw error; + } }; export const encodeBase64 = (content: string) => - Buffer.from(content, "utf-8").toString("base64"); + Buffer.from(content, "utf-8").toString("base64"); export const getCreateFileCommand = ( - outputPath: string, - filePath: string, - content: string + outputPath: string, + filePath: string, + content: string, ) => { - const fullPath = path.join(outputPath, filePath); - if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { - return `mkdir -p ${fullPath};`; - } - - const directory = path.dirname(fullPath); - const encodedContent = encodeBase64(content); - return ` + const fullPath = path.join(outputPath, filePath); + if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { + return `mkdir -p ${fullPath};`; + } + + const directory = path.dirname(fullPath); + const encodedContent = encodeBase64(content); + return ` mkdir -p ${directory}; echo "${encodedContent}" | base64 -d > "${fullPath}"; `; }; export const getServiceContainer = async (appName: string) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; - - const containers = await docker.listContainers({ - filters: JSON.stringify(filter), - }); - - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } - - const container = containers[0]; - - return container; - } catch (error) { - throw error; - } + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; + + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); + + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } + + const container = containers[0]; + + return container; + } catch (error) { + throw error; + } }; export const getRemoteServiceContainer = async ( - serverId: string, - appName: string + serverId: string, + appName: string, ) => { - try { - const filter = { - status: ["running"], - label: [`com.docker.swarm.service.name=${appName}`], - }; - const remoteDocker = await getRemoteDocker(serverId); - const containers = await remoteDocker.listContainers({ - filters: JSON.stringify(filter), - }); - - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } - - const container = containers[0]; - - return container; - } catch (error) { - throw error; - } + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; + const remoteDocker = await getRemoteDocker(serverId); + const containers = await remoteDocker.listContainers({ + filters: JSON.stringify(filter), + }); + + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } + + const container = containers[0]; + + return container; + } catch (error) { + throw error; + } }; From 8bfe1632fa3cca4ecc5165f1751a1a71594ab3ba Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 23 Dec 2024 02:32:43 -0600 Subject: [PATCH 52/85] refactor: remove delete volumes --- packages/server/src/utils/docker/utils.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 1c4a38ab4..a14602e93 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -241,13 +241,7 @@ export const removeService = async ( deleteVolumes = false, ) => { try { - let command: string; - - if (deleteVolumes) { - command = `docker service rm --force ${appName}`; - } else { - command = `docker service rm ${appName}`; - } + const command = `docker service rm ${appName}`; if (serverId) { await execAsyncRemote(serverId, command); From 353effd720670a35dea32131c74a61334fdb02ed Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 23 Dec 2024 02:37:04 -0600 Subject: [PATCH 53/85] refactor: delete log --- packages/server/src/services/compose.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index d38426fd5..ff6603515 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -449,8 +449,6 @@ export const removeCompose = async ( const { COMPOSE_PATH } = paths(!!compose.serverId); const projectPath = join(COMPOSE_PATH, compose.appName); - console.log("API: DELETE VOLUMES=", deleteVolumes); - if (compose.composeType === "stack") { const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; From 0ee5a6f13e0343343958cb8d10dcaedbd4536f05 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 23 Dec 2024 09:16:01 -0500 Subject: [PATCH 54/85] fix(certificates): ensure certificates are accessible by traefik --- packages/server/src/constants/index.ts | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index f2f1a4d88..b6dfd2177 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -5,34 +5,22 @@ export const IS_CLOUD = process.env.IS_CLOUD === "true"; export const docker = new Docker(); export const paths = (isServer = false) => { - if (isServer) { - const BASE_PATH = "/etc/dokploy"; - return { - BASE_PATH, - MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`, - DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`, - LOGS_PATH: `${BASE_PATH}/logs`, - APPLICATIONS_PATH: `${BASE_PATH}/applications`, - COMPOSE_PATH: `${BASE_PATH}/compose`, - SSH_PATH: `${BASE_PATH}/ssh`, - CERTIFICATES_PATH: `${BASE_PATH}/certificates`, - MONITORING_PATH: `${BASE_PATH}/monitoring`, - REGISTRY_PATH: `${BASE_PATH}/registry`, - }; - } const BASE_PATH = - process.env.NODE_ENV === "production" + isServer || process.env.NODE_ENV === "production" ? "/etc/dokploy" : path.join(process.cwd(), ".docker"); + const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`; + const DYNAMIC_TRAEFIK_PATH = `${MAIN_TRAEFIK_PATH}/dynamic`; + return { BASE_PATH, - MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`, - DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`, + MAIN_TRAEFIK_PATH, + DYNAMIC_TRAEFIK_PATH, LOGS_PATH: `${BASE_PATH}/logs`, APPLICATIONS_PATH: `${BASE_PATH}/applications`, COMPOSE_PATH: `${BASE_PATH}/compose`, SSH_PATH: `${BASE_PATH}/ssh`, - CERTIFICATES_PATH: `${BASE_PATH}/certificates`, + CERTIFICATES_PATH: `${DYNAMIC_TRAEFIK_PATH}/certificates`, MONITORING_PATH: `${BASE_PATH}/monitoring`, REGISTRY_PATH: `${BASE_PATH}/registry`, }; From 9c5a61e42f920dd58f30be3c4318ec7d71e36195 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 23 Dec 2024 09:17:11 -0500 Subject: [PATCH 55/85] fix(certificates): show proper placeholder for private key format --- .../dashboard/settings/certificates/add-certificate.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx index 50b2dd909..173141235 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx @@ -45,6 +45,9 @@ import { z } from "zod"; const certificateDataHolder = "-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----"; +const privateKeyDataHolder = + "-----BEGIN PRIVATE KEY-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n-----END PRIVATE KEY-----"; + const addCertificate = z.object({ name: z.string().min(1, "Name is required"), certificateData: z.string().min(1, "Certificate data is required"), @@ -154,7 +157,7 @@ export const AddCertificate = () => {