From b1a48d4636f7d73094d5fef29217978acf988abb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 22 Jul 2024 03:51:07 -0600 Subject: [PATCH 01/79] refactor: update job --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0d93e547..5ac4ef1c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,7 +46,7 @@ jobs: combine-manifests: docker: - - image: cimg/base:stable + - image: cimg/node:18.18.0 steps: - checkout - setup_remote_docker From 63e7eacae90746d5a4f5e59ab0941d968c67c073 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:37:00 -0600 Subject: [PATCH 02/79] chore(version): bump version From 59bb59ee24c859482c7e439b428d23960b8226ba Mon Sep 17 00:00:00 2001 From: limichange Date: Wed, 13 Nov 2024 16:30:11 +0800 Subject: [PATCH 03/79] Update docker-compose.yml --- apps/dokploy/templates/plausible/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/templates/plausible/docker-compose.yml b/apps/dokploy/templates/plausible/docker-compose.yml index 62ce5ece..bb267f65 100644 --- a/apps/dokploy/templates/plausible/docker-compose.yml +++ b/apps/dokploy/templates/plausible/docker-compose.yml @@ -26,7 +26,7 @@ services: hard: 262144 plausible: - image: ghcr.io/plausible/community-edition:v2.1.0 + image: ghcr.io/plausible/community-edition:v2.1.4 restart: always command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" depends_on: From 46f7d43595a7dbe4d377fbf9aee875f1988e63a2 Mon Sep 17 00:00:00 2001 From: limichange Date: Thu, 14 Nov 2024 09:37:20 +0800 Subject: [PATCH 04/79] Update templates.ts --- apps/dokploy/templates/templates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 28af7c56..e328ab0b 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -34,7 +34,7 @@ export const templates: TemplateData[] = [ { id: "plausible", name: "Plausible", - version: "v2.1.0", + version: "v2.1.4", description: "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", logo: "plausible.svg", From a439286e5f7fa242aa4f21318db369d4006d50f0 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Fri, 13 Dec 2024 16:14:52 +0100 Subject: [PATCH 05/79] 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 07f42448..b0b958f2 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 06/79] 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 b0b958f2..7d5ab63b 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 07/79] 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 6d04e815..5f53752b 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 08/79] 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 02bac781..e0c4863b 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 09/79] 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 5ae0d774..c4b88c68 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 10/79] 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 | 21 ++++++----- packages/server/src/services/mongo.ts | 21 ++++++----- 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, 105 insertions(+), 96 deletions(-) diff --git a/packages/server/src/db/schema/utils.ts b/packages/server/src/db/schema/utils.ts index 59ebf4b7..e8651769 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 fef1457c..d2ce3432 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 5ae0d774..27e7bfa4 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 b18b132d..41adf238 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 645b5c65..7906cc5b 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); + const appName = buildAppName("mariadb", input.appName); - if (!valid) { - throw new TRPCError({ - code: "CONFLICT", - message: "Service with this 'AppName' already exists", - }); - } + 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 b87ec4da..82a8a84f 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); + const appName = buildAppName("mongo", 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 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 ee9df820..1bb2c478 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 c94ddbbe..d2cd4874 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 e52f4553..06bf8fb5 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 7809de28..96b3066c 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 11/79] 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 85eb9763..68f5e4e0 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 00000000..fe15d0ef --- /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 6ac61354..8681cb22 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 12/79] 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 ab3dafca..c9392b10 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 00000000..d353ceed --- /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 13/79] 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 00000000..ba2d9e13 --- /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 00000000..1b192f7d --- /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 00000000..2ef632b9 --- /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 00000000..9a092152 --- /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 00000000..ba11d749 --- /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 00000000..d3e99352 --- /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 00000000..6c5bd99d --- /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 14/79] 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 d353ceed..b608efc0 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 15/79] feat: implement fetching of Docker service applications and their details --- packages/server/src/services/docker.ts | 53 +++++++------------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index 8681cb22..c13c71e5 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, 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; - // } + 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, 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; - // } + 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 16/79] refactor: streamline imports and improve code formatting in ShowSwarmNodes component --- .../dashboard/swarm/show/show-nodes.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx b/apps/dokploy/components/dashboard/swarm/show/show-nodes.tsx index 6c5bd99d..e629654f 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 React from "react"; -import { SwarmList, columns } from "./columns"; -import { DataTable } from "./data-table"; 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(); + const { data, isLoading } = api.swarm.getNodes.useQuery(); - console.log(data); - - return ( - - ); + return ( + + ); } export default ShowSwarmNodes; From f98f18b331cce7409ea0422b45f37ab28aa48c35 Mon Sep 17 00:00:00 2001 From: djknaeckebrot Date: Tue, 17 Dec 2024 20:48:35 +0100 Subject: [PATCH 17/79] 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 00000000..88c96c24 --- /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 18/79] chore: remove tables and add new cards --- .../swarm/applications/data-table.tsx | 413 ++++++++---------- .../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, 1570 insertions(+), 703 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 1b192f7d..03915c19 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 { - 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 { + 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[]; + 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 2ef632b9..4363adc1 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 00000000..0ccf8e37 --- /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 00000000..95b4498e --- /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 00000000..dec6c6e7 --- /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 9a092152..4f751805 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 00000000..02b013db --- /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 00000000..95b4498e --- /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 00000000..0486b164 --- /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 ba11d749..b0774936 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 00000000..1d12ab52 --- /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 c9392b10..46e590a7 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 b608efc0..11035da7 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 19/79] 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 1d12ab52..b622ba82 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 20/79] 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 88c96c24..f55ef07b 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 21/79] 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 f55ef07b..16b6102a 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 b622ba82..83d226a5 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 22/79] 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 dec6c6e7..4d6582aa 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 23/79] 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 00000000..10029114 --- /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 00000000..8768a88c --- /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 11035da7..24fa4326 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 24/79] 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 ba2d9e13..1961cd99 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 4363adc1..e3b38a71 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 0ccf8e37..00000000 --- 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 95b4498e..00000000 --- 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 4d6582aa..00000000 --- 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 83d226a5..b8eb9f81 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 4f751805..2d8a3e3e 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 95b4498e..00000000 --- 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 10029114..4b732df4 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 8768a88c..a90546c9 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 0486b164..00000000 --- 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 b0774936..00000000 --- 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 d3e99352..00000000 --- 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 e629654f..00000000 --- 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 24fa4326..b294c09b 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 25/79] 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 7dae38de..63984dec 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 b294c09b..ce4cfce2 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 26/79] 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 63984dec..81a68172 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 a90546c9..bd54f43e 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 ce4cfce2..d8b6b061 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 d22330f9834362079fdf5a4a4108c6baa943cc6f Mon Sep 17 00:00:00 2001 From: Larry Ioannidis Date: Wed, 18 Dec 2024 09:39:20 +0000 Subject: [PATCH 27/79] feat(ports): implement additional ports management --- .../servers/actions/show-traefik-actions.tsx | 9 + .../web-server/manage-traefik-ports.tsx | 215 ++++++++++++++++++ apps/dokploy/public/locales/en/settings.json | 8 + apps/dokploy/server/api/routers/settings.ts | 68 ++++++ packages/server/src/setup/traefik-setup.ts | 11 + 5 files changed, 311 insertions(+) create mode 100644 apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx 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 4385dc6a..546069c5 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 @@ -26,6 +26,7 @@ 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"; interface Props { serverId?: string; @@ -128,6 +129,14 @@ export const ShowTraefikActions = ({ serverId }: Props) => { Enter the terminal */} + + e.preventDefault()} + className="cursor-pointer" + > + {t("settings.server.webServer.traefik.managePorts")} + + 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 new file mode 100644 index 00000000..aa9741ce --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -0,0 +1,215 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + 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"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; + +/** + * Props for the ManageTraefikPorts component + * @interface Props + * @property {React.ReactNode} children - The trigger element that opens the ports management modal + * @property {string} [serverId] - Optional ID of the server whose ports are being managed + */ +interface Props { + children: React.ReactNode; + serverId?: string; +} + +/** + * Represents a port mapping configuration for Traefik + * @interface AdditionalPort + * @property {number} targetPort - The internal port that the service is listening on + * @property {number} publishedPort - The external port that will be exposed + * @property {"ingress" | "host"} publishMode - The Docker Swarm publish mode: + * - "host": Publishes the port directly on the host + * - "ingress": Publishes the port through the Swarm routing mesh + */ +interface AdditionalPort { + targetPort: number; + publishedPort: number; + publishMode: "ingress" | "host"; +} + +/** + * ManageTraefikPorts is a component that provides a modal interface for managing + * additional port mappings for Traefik in a Docker Swarm environment. + * + * Features: + * - Add, remove, and edit port mappings + * - Configure target port, published port, and publish mode for each mapping + * - Persist port configurations through API calls + * + * @component + * @example + * ```tsx + * + * + * + * ``` + */ +export const ManageTraefikPorts = ({ children, serverId }: Props) => { + const { t } = useTranslation("settings"); + const [open, setOpen] = useState(false); + const [additionalPorts, setAdditionalPorts] = useState([]); + + const { data: currentPorts, refetch: refetchPorts } = + api.settings.getTraefikPorts.useQuery({ + serverId, + }); + + const { mutateAsync: updatePorts, isLoading } = + api.settings.updateTraefikPorts.useMutation({ + onSuccess: () => { + refetchPorts(); + }, + }); + + useEffect(() => { + if (currentPorts) { + setAdditionalPorts(currentPorts); + } + }, [currentPorts]); + + const handleAddPort = () => { + setAdditionalPorts([ + ...additionalPorts, + { targetPort: 0, publishedPort: 0, publishMode: "host" }, + ]); + }; + + const handleUpdatePorts = async () => { + try { + await updatePorts({ + serverId, + additionalPorts, + }); + toast.success(t("settings.server.webServer.traefik.portsUpdated")); + setOpen(false); + } catch (error) { + toast.error(t("settings.server.webServer.traefik.portsUpdateError")); + } + }; + + return ( + <> +
setOpen(true)}>{children}
+ + + + + {t("settings.server.webServer.traefik.managePorts")} + + + {t("settings.server.webServer.traefik.managePortsDescription")} + + +
+ {additionalPorts.map((port, index) => ( +
+
+ + { + const newPorts = [...additionalPorts]; + newPorts[index].targetPort = Number.parseInt( + e.target.value, + ); + setAdditionalPorts(newPorts); + }} + className="w-full rounded border p-2" + /> +
+
+ + { + const newPorts = [...additionalPorts]; + newPorts[index].publishedPort = Number.parseInt( + e.target.value, + ); + setAdditionalPorts(newPorts); + }} + className="w-full rounded border p-2" + /> +
+
+ + +
+
+ +
+
+ ))} +
+ + +
+
+
+
+ + ); +}; diff --git a/apps/dokploy/public/locales/en/settings.json b/apps/dokploy/public/locales/en/settings.json index 2103ecc0..1ce54692 100644 --- a/apps/dokploy/public/locales/en/settings.json +++ b/apps/dokploy/public/locales/en/settings.json @@ -18,6 +18,14 @@ "settings.server.webServer.server.label": "Server", "settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.modifyEnv": "Modify Env", + "settings.server.webServer.traefik.managePorts": "Additional Ports", + "settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik", + "settings.server.webServer.traefik.targetPort": "Target Port", + "settings.server.webServer.traefik.publishedPort": "Published Port", + "settings.server.webServer.traefik.addPort": "Add Port", + "settings.server.webServer.traefik.portsUpdated": "Ports updated successfully", + "settings.server.webServer.traefik.portsUpdateError": "Failed to update ports", + "settings.server.webServer.traefik.publishMode": "Publish Mode", "settings.server.webServer.storage.label": "Space", "settings.server.webServer.storage.cleanUnusedImages": "Clean unused images", "settings.server.webServer.storage.cleanUnusedVolumes": "Clean unused volumes", diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 7c777b17..7afdfd18 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -68,6 +68,17 @@ import { publicProcedure, } from "../trpc"; +const apiUpdateTraefikPorts = z.object({ + serverId: z.string().optional(), + additionalPorts: z.array( + z.object({ + targetPort: z.number(), + publishedPort: z.number(), + publishMode: z.enum(["ingress", "host"]).default("host"), + }), + ), +}); + export const settingsRouter = createTRPCRouter({ reloadServer: adminProcedure.mutation(async () => { if (IS_CLOUD) { @@ -706,6 +717,63 @@ export const settingsRouter = createTRPCRouter({ throw new Error("Failed to check GPU status"); } }), + updateTraefikPorts: adminProcedure + .input(apiUpdateTraefikPorts) + .mutation(async ({ input }) => { + try { + await initializeTraefik({ + serverId: input.serverId, + additionalPorts: input.additionalPorts, + }); + return true; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Failed to update Traefik ports", + cause: error, + }); + } + }), + getTraefikPorts: adminProcedure + .input(apiServerSchema) + .query(async ({ input }) => { + const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`; + + try { + let stdout = ""; + if (input?.serverId) { + const result = await execAsyncRemote(input.serverId, command); + stdout = result.stdout; + } else if (!IS_CLOUD) { + const result = await execAsync(command); + stdout = result.stdout; + } + + const ports: Array<{ + Protocol: string; + TargetPort: number; + PublishedPort: number; + PublishMode: string; + }> = JSON.parse(stdout.trim()); + + // Filter out the default ports (80, 443, and optionally 8080) + const additionalPorts = ports + .filter((port) => ![80, 443, 8080].includes(port.PublishedPort)) + .map((port) => ({ + targetPort: port.TargetPort, + publishedPort: port.PublishedPort, + publishMode: port.PublishMode.toLowerCase() as "host" | "ingress", + })); + + return additionalPorts; + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to get Traefik ports", + cause: error, + }); + } + }), }); // { // "Parallelism": 1, diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts index 82832027..7af1b193 100644 --- a/packages/server/src/setup/traefik-setup.ts +++ b/packages/server/src/setup/traefik-setup.ts @@ -16,12 +16,18 @@ interface TraefikOptions { enableDashboard?: boolean; env?: string[]; serverId?: string; + additionalPorts?: Array<{ + targetPort: number; + publishedPort: number; + publishMode?: "ingress" | "host"; + }>; } export const initializeTraefik = async ({ enableDashboard = false, env, serverId, + additionalPorts = [], }: TraefikOptions = {}) => { const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId); const imageName = "traefik:v3.1.2"; @@ -84,6 +90,11 @@ export const initializeTraefik = async ({ }, ] : []), + ...additionalPorts.map((port) => ({ + TargetPort: port.targetPort, + PublishedPort: port.publishedPort, + PublishMode: port.publishMode || ("host" as const), + })), ], }, }; From 8ea453f4440f39226b5e0d42684aee5baf50c7c7 Mon Sep 17 00:00:00 2001 From: DJKnaeckebrot Date: Wed, 18 Dec 2024 13:01:09 +0100 Subject: [PATCH 28/79] 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 | 808 +++++++++--------- 4 files changed, 446 insertions(+), 403 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/delete-application.tsx b/apps/dokploy/components/dashboard/application/delete-application.tsx index f34d29a7..ff63ef5c 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 2902c8ed..9b16d579 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 d9b1a5df..923ea130 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 216ee867..e8c9e6c2 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"); - } + 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; - } + 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"); - } + try { + if (!dockerImage) { + throw new Error("Docker image not found"); + } - const remoteDocker = await getRemoteDocker(serverId); + const remoteDocker = await getRemoteDocker(serverId); - await new Promise((resolve, reject) => { - remoteDocker.pull( - dockerImage, - { authconfig: authConfig }, - (err, stream) => { - if (err) { - reject(err); - return; - } + 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; - } + 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", - ); + 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; - } + 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 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}`; - }); + 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; + 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 []; - } + 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, - })); + 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 { + healthCheckSwarm, + restartPolicySwarm, + placementSwarm, + updateConfigSwarm, + rollbackConfigSwarm, + modeSwarm, + labelsSwarm, + replicas, + mounts, + networkSwarm, + } = application; - const haveMounts = mounts.length > 0; + 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" }], - }), - }; + 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 []; - } + 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, - })); + 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 []; - } + 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, - }; - }); + 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; - } + 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; - } + 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 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 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}`], - }; + try { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; - const containers = await docker.listContainers({ - filters: JSON.stringify(filter), - }); + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); - if (containers.length === 0 || !containers[0]) { - throw new Error(`No container found with name: ${appName}`); - } + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } - const container = containers[0]; + const container = containers[0]; - return container; - } catch (error) { - throw error; - } + 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), - }); + 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}`); - } + if (containers.length === 0 || !containers[0]) { + throw new Error(`No container found with name: ${appName}`); + } - const container = containers[0]; + const container = containers[0]; - return container; - } catch (error) { - throw error; - } + return container; + } catch (error) { + throw error; + } }; From 3858205e520c44a24c912f58565c50b009d45d9f Mon Sep 17 00:00:00 2001 From: 190km Date: Wed, 18 Dec 2024 22:36:57 +0100 Subject: [PATCH 29/79] 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 00000000..c5b381eb --- /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 4eb2107f..bff6c929 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 45451e78..7be497ed 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 30/79] 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 c5b381eb..521f0afe 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 7be497ed..2500662d 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 31/79] 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 521f0afe..c5b381eb 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 2500662d..0c060afd 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 d9a1976cc0a3a1113232078c1c751c7097f8053e Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 14:01:55 +0100 Subject: [PATCH 32/79] fix: check updates message fixes --- .../dashboard/settings/web-server/update-server.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 48a61c7a..a2e4a9fa 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -76,18 +76,18 @@ export const UpdateServer = () => { className="w-full" onClick={async () => { await checkAndUpdateImage() - .then(async (e) => { - setIsUpdateAvailable(e); + .then(async (updateAvailable) => { + setIsUpdateAvailable(updateAvailable); + toast.info(updateAvailable ? "Update is available" : "No updates available"); }) .catch(() => { setIsUpdateAvailable(false); - toast.error("Error to check updates"); + toast.error("An error occurred while checking for updates, please try again."); }); - toast.success("Check updates"); }} isLoading={isLoading} > - Check Updates + {isLoading ? "Checking for updates..." : "Check for updates"} )}
From dd64b063408432f2bcd0a81824b0e0d5b8841d91 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 14:09:05 +0100 Subject: [PATCH 33/79] style: format with biome --- .../dashboard/settings/web-server/update-server.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index a2e4a9fa..06d4a3f1 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -78,11 +78,17 @@ export const UpdateServer = () => { await checkAndUpdateImage() .then(async (updateAvailable) => { setIsUpdateAvailable(updateAvailable); - toast.info(updateAvailable ? "Update is available" : "No updates available"); + toast.info( + updateAvailable + ? "Update is available" + : "No updates available", + ); }) .catch(() => { setIsUpdateAvailable(false); - toast.error("An error occurred while checking for updates, please try again."); + toast.error( + "An error occurred while checking for updates, please try again.", + ); }); }} isLoading={isLoading} From b842887bc388f32726c77ba2c4431a83c4720a77 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 16:43:05 +0100 Subject: [PATCH 34/79] feat: add toggle for auto updates checking --- .../web-server/toggle-auto-check-updates.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx diff --git a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx new file mode 100644 index 00000000..d115672a --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx @@ -0,0 +1,27 @@ +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { useState } from "react"; + +export const ToggleAutoCheckUpdates = () => { + const [enabled, setEnabled] = useState( + localStorage.getItem("enableAutoCheckUpdates") === "true", + ); + + const handleToggle = async (checked: boolean) => { + setEnabled(checked); + localStorage.setItem("enableAutoCheckUpdates", String(checked)); + }; + + return ( +
+ + +
+ ); +}; From a5cd8f18cdc9d3ff09f792721701e3e49487b271 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:23:02 +0100 Subject: [PATCH 35/79] feat: show auto check update toggle --- .../dashboard/settings/web-server/update-server.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 06d4a3f1..8d1ed2e0 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -14,13 +14,14 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { UpdateWebServer } from "./update-webserver"; +import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates"; export const UpdateServer = () => { const [isUpdateAvailable, setIsUpdateAvailable] = useState( null, ); - const { mutateAsync: checkAndUpdateImage, isLoading } = - api.settings.checkAndUpdateImage.useMutation(); + const { mutateAsync: checkServerUpdates, isLoading } = + api.settings.checkServerUpdates.useMutation(); const [isOpen, setIsOpen] = useState(false); return ( @@ -61,6 +62,7 @@ export const UpdateServer = () => {
+ {isUpdateAvailable === false && (
@@ -75,7 +77,7 @@ export const UpdateServer = () => { @@ -36,19 +63,12 @@ export const UpdateWebServer = () => { Are you absolutely sure? This action cannot be undone. This will update the web server to the - new version. + new version. The page will be reloaded once the update is finished. Cancel - { - await updateServer(); - toast.success("Please reload the browser to see the changes"); - }} - > - Confirm - + Confirm From 2804748118511dd0a46b48c202c94f6d419ed94c Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:27:51 +0100 Subject: [PATCH 38/79] refactor: rename action, move pull to updateServer --- apps/dokploy/server/api/routers/settings.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 7c777b17..001da65e 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -45,6 +45,7 @@ import { stopService, stopServiceRemote, updateAdmin, + checkIsUpdateAvailable, updateLetsEncryptEmail, updateServerById, updateServerTraefik, @@ -342,17 +343,20 @@ export const settingsRouter = createTRPCRouter({ writeConfig("middlewares", input.traefikConfig); return true; }), - - checkAndUpdateImage: adminProcedure.mutation(async () => { + checkForUpdate: adminProcedure.mutation(async () => { if (IS_CLOUD) { return true; } - return await pullLatestRelease(); + + return await checkIsUpdateAvailable(); }), updateServer: adminProcedure.mutation(async () => { if (IS_CLOUD) { return true; } + + await pullLatestRelease(); + await spawnAsync("docker", [ "service", "update", @@ -361,6 +365,7 @@ export const settingsRouter = createTRPCRouter({ getDokployImage(), "dokploy", ]); + return true; }), From 256534570b450c4dade3e27e2728a080c17ada32 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:29:01 +0100 Subject: [PATCH 39/79] refactor: add image tag helper, refactor update check logic, remove try/catch --- packages/server/src/services/settings.ts | 54 +++++++++++++++--------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 8261843a..fb8e6a65 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -3,37 +3,49 @@ import { join } from "node:path"; import { docker } from "@dokploy/server/constants"; import { getServiceContainer } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; +import { spawnAsync } from "../utils/process/spawnAsync"; // import packageInfo from "../../../package.json"; -const updateIsAvailable = async () => { - try { - const service = await getServiceContainer("dokploy"); +/** Returns current Dokploy docker image tag or `latest` by default. */ +export const getDokployImageTag = () => { + return process.env.RELEASE_TAG || "latest"; +}; - const localImage = await docker.getImage(getDokployImage()).inspect(); - return localImage.Id !== service?.ImageID; - } catch (error) { - return false; - } +/** Checks if server update is available by comparing current image's digest against digest for provided image tag via Docker hub API */ +export const checkIsUpdateAvailable = async () => { + const commandResult = await spawnAsync("docker", [ + "inspect", + "--format={{index .RepoDigests 0}}", + getDokployImage(), + ]); + + const currentDigest = commandResult.toString().trim().split("@")[1]; + + const url = `https://hub.docker.com/v2/repositories/dokploy/dokploy/tags/${getDokployImageTag()}`; + const response = await fetch(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + const data = (await response.json()) as { digest: string }; + const { digest } = data; + + return digest !== currentDigest; }; export const getDokployImage = () => { - return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`; + return `dokploy/dokploy:${getDokployImageTag()}`; }; export const pullLatestRelease = async () => { - try { - const stream = await docker.pull(getDokployImage(), {}); - await new Promise((resolve, reject) => { - docker.modem.followProgress(stream, (err, res) => - err ? reject(err) : resolve(res), - ); - }); - const newUpdateIsAvailable = await updateIsAvailable(); - return newUpdateIsAvailable; - } catch (error) {} - - return false; + const stream = await docker.pull(getDokployImage()); + await new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (err, res) => + err ? reject(err) : resolve(res), + ); + }); }; + export const getDokployVersion = () => { // return packageInfo.version; }; From a06dd17aa136bab2276b24c7e8ecdbff6ca160cc Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:30:14 +0100 Subject: [PATCH 40/79] feat(navbar): add automatic update checking interval, add update available button --- apps/dokploy/components/layouts/navbar.tsx | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index cead4683..b0836939 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -15,8 +15,13 @@ import { useRouter } from "next/router"; import { Logo } from "../shared/logo"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { buttonVariants } from "../ui/button"; +import { useEffect, useRef, useState } from "react"; +import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver"; + +const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 15; export const Navbar = () => { + const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); const router = useRouter(); const { data } = api.auth.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -29,6 +34,55 @@ export const Navbar = () => { }, ); const { mutateAsync } = api.auth.logout.useMutation(); + const { mutateAsync: checkForUpdate } = + api.settings.checkForUpdate.useMutation(); + + const checkUpdatesIntervalRef = useRef(null); + + useEffect(() => { + // Handling of automatic check for server updates + if (!localStorage.getItem("enableAutoCheckUpdates")) { + // Enable auto update checking by default if user didn't change it + localStorage.setItem("enableAutoCheckUpdates", "true"); + } + + const clearUpdatesInterval = () => { + if (checkUpdatesIntervalRef.current) { + clearInterval(checkUpdatesIntervalRef.current); + } + }; + + const checkUpdates = async () => { + try { + if (localStorage.getItem("enableAutoCheckUpdates") !== "true") { + return; + } + + const updateAvailable = await checkForUpdate(); + + if (updateAvailable) { + // Stop interval when update is available + clearUpdatesInterval(); + setIsUpdateAvailable(true); + } + } catch (error) { + console.error("Error auto-checking for updates:", error); + } + }; + + checkUpdatesIntervalRef.current = setInterval( + checkUpdates, + AUTO_CHECK_UPDATES_INTERVAL_MINUTES * 60000, + ); + + // Also check for updates on initial page load + checkUpdates(); + + return () => { + clearUpdatesInterval(); + }; + }, []); + return (
+ {isUpdateAvailable && ( +
+ +
+ )} Date: Fri, 20 Dec 2024 17:32:10 +0100 Subject: [PATCH 41/79] refactor: remove unused async --- .../dashboard/settings/web-server/toggle-auto-check-updates.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx index d115672a..5c07d5df 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx @@ -7,7 +7,7 @@ export const ToggleAutoCheckUpdates = () => { localStorage.getItem("enableAutoCheckUpdates") === "true", ); - const handleToggle = async (checked: boolean) => { + const handleToggle = (checked: boolean) => { setEnabled(checked); localStorage.setItem("enableAutoCheckUpdates", String(checked)); }; From 4565b3d7a2af0681f7e8a419e12a0910c529b99c Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 18:26:54 +0100 Subject: [PATCH 42/79] refactor: add latestVersion information to update data --- .../settings/web-server/update-server.tsx | 8 ++--- apps/dokploy/components/layouts/navbar.tsx | 6 ++-- apps/dokploy/server/api/routers/settings.ts | 8 ++--- packages/server/src/services/settings.ts | 35 +++++++++++++++---- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 14a66749..1b8798e4 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -20,16 +20,16 @@ export const UpdateServer = () => { const [isUpdateAvailable, setIsUpdateAvailable] = useState( null, ); - const { mutateAsync: checkForUpdate, isLoading } = - api.settings.checkForUpdate.useMutation(); + const { mutateAsync: getUpdateData, isLoading } = + api.settings.getUpdateData.useMutation(); const [isOpen, setIsOpen] = useState(false); const handleCheckUpdates = async () => { try { - const updateAvailable = await checkForUpdate(); + const { updateAvailable, latestVersion } = await getUpdateData(); setIsUpdateAvailable(updateAvailable); if (updateAvailable) { - toast.success("Update is available!"); + toast.success(`${latestVersion} update is available!`); } else { toast.info("No updates available"); } diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index b0836939..a5a8b74c 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -34,8 +34,8 @@ export const Navbar = () => { }, ); const { mutateAsync } = api.auth.logout.useMutation(); - const { mutateAsync: checkForUpdate } = - api.settings.checkForUpdate.useMutation(); + const { mutateAsync: getUpdateData } = + api.settings.getUpdateData.useMutation(); const checkUpdatesIntervalRef = useRef(null); @@ -58,7 +58,7 @@ export const Navbar = () => { return; } - const updateAvailable = await checkForUpdate(); + const { updateAvailable } = await getUpdateData(); if (updateAvailable) { // Stop interval when update is available diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 001da65e..2861511e 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -45,7 +45,7 @@ import { stopService, stopServiceRemote, updateAdmin, - checkIsUpdateAvailable, + getUpdateData, updateLetsEncryptEmail, updateServerById, updateServerTraefik, @@ -343,12 +343,12 @@ export const settingsRouter = createTRPCRouter({ writeConfig("middlewares", input.traefikConfig); return true; }), - checkForUpdate: adminProcedure.mutation(async () => { + getUpdateData: adminProcedure.mutation(async () => { if (IS_CLOUD) { - return true; + return { latestVersion: null, updateAvailable: false }; } - return await checkIsUpdateAvailable(); + return await getUpdateData(); }), updateServer: adminProcedure.mutation(async () => { if (IS_CLOUD) { diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index fb8e6a65..3000f832 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -1,7 +1,6 @@ import { readdirSync } from "node:fs"; import { join } from "node:path"; import { docker } from "@dokploy/server/constants"; -import { getServiceContainer } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { spawnAsync } from "../utils/process/spawnAsync"; // import packageInfo from "../../../package.json"; @@ -11,8 +10,11 @@ export const getDokployImageTag = () => { return process.env.RELEASE_TAG || "latest"; }; -/** Checks if server update is available by comparing current image's digest against digest for provided image tag via Docker hub API */ -export const checkIsUpdateAvailable = async () => { +/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ +export const getUpdateData = async (): Promise<{ + latestVersion: string | null; + updateAvailable: boolean; +}> => { const commandResult = await spawnAsync("docker", [ "inspect", "--format={{index .RepoDigests 0}}", @@ -21,16 +23,35 @@ export const checkIsUpdateAvailable = async () => { const currentDigest = commandResult.toString().trim().split("@")[1]; - const url = `https://hub.docker.com/v2/repositories/dokploy/dokploy/tags/${getDokployImageTag()}`; + const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); - const data = (await response.json()) as { digest: string }; - const { digest } = data; + const data = (await response.json()) as { + results: [{ digest: string; name: string }]; + }; + const { results } = data; + const latestTagDigest = results.find((t) => t.name === "latest")?.digest; - return digest !== currentDigest; + if (!latestTagDigest) { + return { latestVersion: null, updateAvailable: false }; + } + + const versionedTag = results.find( + (t) => t.digest === latestTagDigest && t.name.startsWith("v"), + ); + + if (!versionedTag) { + return { latestVersion: null, updateAvailable: false }; + } + + const { name: latestVersion, digest } = versionedTag; + + const updateAvailable = digest !== currentDigest; + + return { latestVersion, updateAvailable }; }; export const getDokployImage = () => { From ab9aa56c48ba7eb15c6a3c5f2bb05f3b83cf253d Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 18:57:28 +0100 Subject: [PATCH 43/79] refactor: disable automatic updates for cloud version --- apps/dokploy/components/layouts/navbar.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index a5a8b74c..3d17b3e9 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -41,6 +41,10 @@ export const Navbar = () => { useEffect(() => { // Handling of automatic check for server updates + if (isCloud) { + return; + } + if (!localStorage.getItem("enableAutoCheckUpdates")) { // Enable auto update checking by default if user didn't change it localStorage.setItem("enableAutoCheckUpdates", "true"); From f40e80233174ff4532da490bd1b17a79b00a60d3 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 08:47:21 +0100 Subject: [PATCH 44/79] fix: pull latest release in case of no image when checking update --- packages/server/src/services/settings.ts | 54 ++++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 3000f832..b77dbbc9 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -10,11 +10,21 @@ export const getDokployImageTag = () => { return process.env.RELEASE_TAG || "latest"; }; -/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ -export const getUpdateData = async (): Promise<{ - latestVersion: string | null; - updateAvailable: boolean; -}> => { +export const getDokployImage = () => { + return `dokploy/dokploy:${getDokployImageTag()}`; +}; + +export const pullLatestRelease = async () => { + const stream = await docker.pull(getDokployImage()); + await new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (err, res) => + err ? reject(err) : resolve(res), + ); + }); +}; + +/** Returns current docker image digest */ +export const getCurrentImageDigest = async () => { const commandResult = await spawnAsync("docker", [ "inspect", "--format={{index .RepoDigests 0}}", @@ -23,6 +33,27 @@ export const getUpdateData = async (): Promise<{ const currentDigest = commandResult.toString().trim().split("@")[1]; + return currentDigest; +}; + +/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ +export const getUpdateData = async (): Promise<{ + latestVersion: string | null; + updateAvailable: boolean; +}> => { + let currentDigest: string | undefined; + try { + currentDigest = await getCurrentImageDigest(); + } catch { + // In case image doesn't exist yet, pull latest release + await pullLatestRelease(); + currentDigest = await getCurrentImageDigest(); + } + + if (!currentDigest) { + throw new Error("Could not get current image digest"); + } + const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { method: "GET", @@ -54,19 +85,6 @@ export const getUpdateData = async (): Promise<{ return { latestVersion, updateAvailable }; }; -export const getDokployImage = () => { - return `dokploy/dokploy:${getDokployImageTag()}`; -}; - -export const pullLatestRelease = async () => { - const stream = await docker.pull(getDokployImage()); - await new Promise((resolve, reject) => { - docker.modem.followProgress(stream, (err, res) => - err ? reject(err) : resolve(res), - ); - }); -}; - export const getDokployVersion = () => { // return packageInfo.version; }; From 18eae9f7d73f2ac15046861ae5aa1f7a4a838361 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 09:04:25 +0100 Subject: [PATCH 45/79] refactor: use service image sha instead of image itself for checking updates --- packages/server/src/services/settings.ts | 34 ++++++++---------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index b77dbbc9..a3bd3f09 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -1,8 +1,10 @@ import { readdirSync } from "node:fs"; import { join } from "node:path"; import { docker } from "@dokploy/server/constants"; -import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; -import { spawnAsync } from "../utils/process/spawnAsync"; +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; // import packageInfo from "../../../package.json"; /** Returns current Dokploy docker image tag or `latest` by default. */ @@ -23,15 +25,12 @@ export const pullLatestRelease = async () => { }); }; -/** Returns current docker image digest */ -export const getCurrentImageDigest = async () => { - const commandResult = await spawnAsync("docker", [ - "inspect", - "--format={{index .RepoDigests 0}}", - getDokployImage(), - ]); - - const currentDigest = commandResult.toString().trim().split("@")[1]; +/** Returns Dokploy docker service image digest */ +export const getServiceImageDigest = async () => { + const { stdout } = await execAsync( + "docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'", + ); + const currentDigest = stdout.trim().split("@")[1]; return currentDigest; }; @@ -41,18 +40,7 @@ export const getUpdateData = async (): Promise<{ latestVersion: string | null; updateAvailable: boolean; }> => { - let currentDigest: string | undefined; - try { - currentDigest = await getCurrentImageDigest(); - } catch { - // In case image doesn't exist yet, pull latest release - await pullLatestRelease(); - currentDigest = await getCurrentImageDigest(); - } - - if (!currentDigest) { - throw new Error("Could not get current image digest"); - } + const currentDigest = await getServiceImageDigest(); const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { From 8699e024ee1f80e53bd9495d7c8776aebfddaa5d Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 10:05:31 +0100 Subject: [PATCH 46/79] refactor: add try catch, add default update data --- apps/dokploy/server/api/routers/settings.ts | 3 +- packages/server/src/services/settings.ts | 34 ++++++++++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 2861511e..937c93d3 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -52,6 +52,7 @@ import { writeConfig, writeMainConfig, writeTraefikConfigInPath, + DEFAULT_UPDATE_DATA, } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; @@ -345,7 +346,7 @@ export const settingsRouter = createTRPCRouter({ }), getUpdateData: adminProcedure.mutation(async () => { if (IS_CLOUD) { - return { latestVersion: null, updateAvailable: false }; + return DEFAULT_UPDATE_DATA; } return await getUpdateData(); diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index a3bd3f09..0d2ec967 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -7,6 +7,16 @@ import { } from "@dokploy/server/utils/process/execAsync"; // import packageInfo from "../../../package.json"; +export interface IUpdateData { + latestVersion: string | null; + updateAvailable: boolean; +} + +export const DEFAULT_UPDATE_DATA: IUpdateData = { + latestVersion: null, + updateAvailable: false, +}; + /** Returns current Dokploy docker image tag or `latest` by default. */ export const getDokployImageTag = () => { return process.env.RELEASE_TAG || "latest"; @@ -30,17 +40,27 @@ export const getServiceImageDigest = async () => { const { stdout } = await execAsync( "docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'", ); + const currentDigest = stdout.trim().split("@")[1]; + if (!currentDigest) { + throw new Error("Could not get current service image digest"); + } + return currentDigest; }; /** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ -export const getUpdateData = async (): Promise<{ - latestVersion: string | null; - updateAvailable: boolean; -}> => { - const currentDigest = await getServiceImageDigest(); +export const getUpdateData = async (): Promise => { + let currentDigest: string; + try { + currentDigest = await getServiceImageDigest(); + } catch { + // Docker service might not exist locally + // You can run the # Installation command for docker service create mentioned in the below docs to test it locally: + // https://docs.dokploy.com/docs/core/manual-installation + return DEFAULT_UPDATE_DATA; + } const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { @@ -55,7 +75,7 @@ export const getUpdateData = async (): Promise<{ const latestTagDigest = results.find((t) => t.name === "latest")?.digest; if (!latestTagDigest) { - return { latestVersion: null, updateAvailable: false }; + return DEFAULT_UPDATE_DATA; } const versionedTag = results.find( @@ -63,7 +83,7 @@ export const getUpdateData = async (): Promise<{ ); if (!versionedTag) { - return { latestVersion: null, updateAvailable: false }; + return DEFAULT_UPDATE_DATA; } const { name: latestVersion, digest } = versionedTag; From a8ff6c7b3f83c6c41a72462b065500f9116fedcf Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Sat, 21 Dec 2024 11:03:41 -0500 Subject: [PATCH 47/79] feat(updates): new update UI --- .../settings/web-server/update-server.tsx | 201 +++++++++++++----- .../settings/web-server/update-webserver.tsx | 4 +- apps/dokploy/components/layouts/navbar.tsx | 4 +- apps/dokploy/server/api/routers/settings.ts | 4 +- 4 files changed, 158 insertions(+), 55 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 1b8798e4..1bb240bb 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -3,38 +3,49 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, - DialogDescription, - DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { RefreshCcw } from "lucide-react"; +import { + Bug, + Download, + Info, + RefreshCcw, + Server, + Sparkles, + Stars, +} from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; -import { UpdateWebServer } from "./update-webserver"; import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates"; +import { UpdateWebServer } from "./update-webserver"; export const UpdateServer = () => { - const [isUpdateAvailable, setIsUpdateAvailable] = useState( - null, - ); + const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false); + const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); const { mutateAsync: getUpdateData, isLoading } = api.settings.getUpdateData.useMutation(); + const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const [isOpen, setIsOpen] = useState(false); + const [latestVersion, setLatestVersion] = useState(""); const handleCheckUpdates = async () => { try { - const { updateAvailable, latestVersion } = await getUpdateData(); - setIsUpdateAvailable(updateAvailable); - if (updateAvailable) { - toast.success(`${latestVersion} update is available!`); + const updateData = await getUpdateData(); + setHasCheckedUpdate(true); + setIsUpdateAvailable(updateData.updateAvailable); + setLatestVersion(updateData.latestVersion || ""); + + if (updateData.updateAvailable) { + toast.success(`${updateData.latestVersion || ""} update is available!`); } else { toast.info("No updates available"); } } catch (error) { console.error("Error checking for updates:", error); + setHasCheckedUpdate(true); setIsUpdateAvailable(false); toast.error( "An error occurred while checking for updates, please try again.", @@ -45,59 +56,147 @@ export const UpdateServer = () => { return ( - - - - Web Server Update - - Check new releases and update your dokploy - - + +
+ + Web Server Update + + {dokployVersion && ( +
+ + {dokployVersion} +
+ )} +
-
- - We suggest to update your dokploy to the latest version only if you: - -
    -
  • Want to try the latest features
  • -
  • Some bug that is blocking to use some features
  • -
- - We recommend checking the latest version for any breaking changes - before updating. Go to{" "} - - Dokploy Releases - {" "} - to check the latest version. - + {/* Initial state */} + {!hasCheckedUpdate && ( +
+

+ Check for new releases and update Dokploy. +
+
+ We recommend checking for updates regularly to ensure you have the + latest features and security improvements. +

+
+ )} -
- - {isUpdateAvailable === false && ( -
- - - You are using the latest version + {/* Update available state */} + {isUpdateAvailable && latestVersion && ( +
+
+
+ + + + + + + New version available:
- )} + + {latestVersion} + +
+ +
+

+ A new version of the server software is available. Consider + updating if you: +

+
    +
  • + + + Want to access the latest features and improvements + +
  • +
  • + + + Are experiencing issues that may be resolved in the new + version + +
  • +
+
+
+ )} + + {/* Up to date state */} + {hasCheckedUpdate && !isUpdateAvailable && !isLoading && ( +
+
+
+ +
+
+

+ You are using the latest version +

+

+ Your server is up to date with all the latest features and + security improvements. +

+
+
+
+ )} + + {isUpdateAvailable && ( +
+
+ +
+ We recommend reviewing the{" "} + + release notes + {" "} + for any breaking changes before updating. +
+
+
+ )} + +
+ +
+ +
+
+ {isUpdateAvailable ? ( ) : ( )}
@@ -106,3 +205,5 @@ export const UpdateServer = () => {
); }; + +export default UpdateServer; diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx index 9b3c89f6..c1e5de70 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; +import { HardDriveDownload } from "lucide-react"; import { toast } from "sonner"; interface Props { @@ -21,7 +22,7 @@ export const UpdateWebServer = ({ isNavbar }: Props) => { const { mutateAsync: updateServer, isLoading } = api.settings.updateServer.useMutation(); - const buttonLabel = isNavbar ? "Update available" : "Update server"; + const buttonLabel = isNavbar ? "Update available" : "Update Server"; const handleConfirm = async () => { try { @@ -49,6 +50,7 @@ export const UpdateWebServer = ({ isNavbar }: Props) => { variant={isNavbar ? "outline" : "secondary"} isLoading={isLoading} > + {!isLoading && } {!isLoading && ( diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index 3d17b3e9..1a7da0ea 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -12,11 +12,11 @@ import { api } from "@/utils/api"; import { HeartIcon } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { useEffect, useRef, useState } from "react"; +import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver"; import { Logo } from "../shared/logo"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { buttonVariants } from "../ui/button"; -import { useEffect, useRef, useState } from "react"; -import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver"; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 15; diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 937c93d3..e30cee4a 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -12,6 +12,7 @@ import { } from "@/server/db/schema"; import { removeJob, schedule } from "@/server/utils/backup"; import { + DEFAULT_UPDATE_DATA, IS_CLOUD, canAccessToTraefikFiles, cleanStoppedContainers, @@ -25,6 +26,7 @@ import { findAdminById, findServerById, getDokployImage, + getUpdateData, initializeTraefik, logRotationManager, parseRawConfig, @@ -45,14 +47,12 @@ import { stopService, stopServiceRemote, updateAdmin, - getUpdateData, updateLetsEncryptEmail, updateServerById, updateServerTraefik, writeConfig, writeMainConfig, writeTraefikConfigInPath, - DEFAULT_UPDATE_DATA, } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; From 6c9b12cee904072e6a46d41b9fc3054ac4b79d21 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 18:33:22 +0100 Subject: [PATCH 48/79] refactor: use dynamic tag for comparing latest tag digest --- packages/server/src/services/settings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 0d2ec967..0ab9744e 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -72,7 +72,9 @@ export const getUpdateData = async (): Promise => { results: [{ digest: string; name: string }]; }; const { results } = data; - const latestTagDigest = results.find((t) => t.name === "latest")?.digest; + const latestTagDigest = results.find( + (t) => t.name === getDokployImageTag(), + )?.digest; if (!latestTagDigest) { return DEFAULT_UPDATE_DATA; From d08530d4516945a0bc77602fe9d213f8fcda08da Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Sat, 21 Dec 2024 12:22:01 -0500 Subject: [PATCH 49/79] feat(updates): clean up light mode --- .../web-server/toggle-auto-check-updates.tsx | 3 +- .../settings/web-server/update-server.tsx | 59 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx index 5c07d5df..fb3776b1 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx @@ -2,7 +2,7 @@ import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useState } from "react"; -export const ToggleAutoCheckUpdates = () => { +export const ToggleAutoCheckUpdates = ({ disabled }: { disabled: boolean }) => { const [enabled, setEnabled] = useState( localStorage.getItem("enableAutoCheckUpdates") === "true", ); @@ -18,6 +18,7 @@ export const ToggleAutoCheckUpdates = () => { checked={enabled} onCheckedChange={handleToggle} id="autoCheckUpdatesToggle" + disabled={disabled} />