From b14b9300c02de97ab6f935cfa20719495cf020f7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 1 Jun 2025 22:52:33 -0600 Subject: [PATCH] feat: enhance rollback functionality with UI updates and database schema changes - Updated Tailwind configuration for responsive design. - Modified the ShowDeployments component to include rollback settings and actions. - Introduced a new "rollback" table in the database schema with foreign key relationships. - Updated deployment and application schemas to support rollback features. - Added rollback mutation to the API for initiating rollbacks. --- .../deployments/show-deployments.tsx | 73 ++++++++++++++++--- .../drizzle/0093_funny_leper_queen.sql | 14 ++++ .../drizzle/0093_yielding_typhoid_mary.sql | 12 --- apps/dokploy/drizzle/meta/0093_snapshot.json | 35 +++++++-- apps/dokploy/drizzle/meta/_journal.json | 4 +- apps/dokploy/server/api/routers/deployment.ts | 4 + apps/dokploy/server/api/routers/rollbacks.ts | 48 ++++++------ apps/dokploy/tailwind.config.ts | 2 +- packages/server/src/db/schema/application.ts | 2 - packages/server/src/db/schema/deployment.ts | 9 +++ packages/server/src/db/schema/rollbacks.ts | 12 +-- packages/server/src/services/application.ts | 4 +- packages/server/src/services/deployment.ts | 24 ++++-- packages/server/src/services/rollbacks.ts | 27 ++++++- 14 files changed, 192 insertions(+), 78 deletions(-) create mode 100644 apps/dokploy/drizzle/0093_funny_leper_queen.sql delete mode 100644 apps/dokploy/drizzle/0093_yielding_typhoid_mary.sql diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 3cb18f98..a0f28dbb 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -10,11 +10,20 @@ import { CardTitle, } from "@/components/ui/card"; import { type RouterOutputs, api } from "@/utils/api"; -import { Clock, Loader2, RocketIcon } from "lucide-react"; +import { + Clock, + Loader2, + RocketIcon, + Settings, + ArrowDownToLine, +} from "lucide-react"; import React, { useEffect, useState } from "react"; import { CancelQueues } from "./cancel-queues"; import { RefreshToken } from "./refresh-token"; import { ShowDeployment } from "./show-deployment"; +import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { toast } from "sonner"; interface Props { id: string; @@ -57,6 +66,9 @@ export const ShowDeployments = ({ }, ); + const { mutateAsync: rollback, isLoading: isRollingBack } = + api.rollback.rollback.useMutation(); + const [url, setUrl] = React.useState(""); useEffect(() => { setUrl(document.location.origin); @@ -71,9 +83,18 @@ export const ShowDeployments = ({ See all the 10 last deployments for this {type} - {(type === "application" || type === "compose") && ( - - )} +
+ {(type === "application" || type === "compose") && ( + + )} + {type === "application" && ( + + + + )} +
{refreshToken && ( @@ -154,13 +175,43 @@ export const ShowDeployments = ({ )} - +
+ + + {deployment?.rollback && ( + { + await rollback({ + rollbackId: deployment.rollback.rollbackId, + }) + .then(() => { + toast.success("Rollback initiated successfully"); + }) + .catch(() => { + toast.error("Error initiating rollback"); + }); + }} + > + + + )} +
))} diff --git a/apps/dokploy/drizzle/0093_funny_leper_queen.sql b/apps/dokploy/drizzle/0093_funny_leper_queen.sql new file mode 100644 index 00000000..38b0bb5f --- /dev/null +++ b/apps/dokploy/drizzle/0093_funny_leper_queen.sql @@ -0,0 +1,14 @@ +CREATE TABLE "rollback" ( + "rollbackId" text PRIMARY KEY NOT NULL, + "env" text, + "deploymentId" text NOT NULL, + "version" serial NOT NULL, + "image" text, + "createdAt" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "rollbackActive" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "application" ADD COLUMN "limitRollback" integer DEFAULT 5;--> statement-breakpoint +ALTER TABLE "deployment" ADD COLUMN "rollbackId" text;--> statement-breakpoint +ALTER TABLE "rollback" ADD CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "deployment" ADD CONSTRAINT "deployment_rollbackId_rollback_rollbackId_fk" FOREIGN KEY ("rollbackId") REFERENCES "public"."rollback"("rollbackId") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/0093_yielding_typhoid_mary.sql b/apps/dokploy/drizzle/0093_yielding_typhoid_mary.sql deleted file mode 100644 index a66b6de9..00000000 --- a/apps/dokploy/drizzle/0093_yielding_typhoid_mary.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE "rollback" ( - "rollbackId" text PRIMARY KEY NOT NULL, - "env" text, - "applicationId" text NOT NULL, - "version" serial NOT NULL, - "image" text, - "createdAt" text NOT NULL -); ---> statement-breakpoint -ALTER TABLE "application" ADD COLUMN "rollbackActive" boolean DEFAULT false;--> statement-breakpoint -ALTER TABLE "application" ADD COLUMN "limitRollback" integer DEFAULT 5;--> statement-breakpoint -ALTER TABLE "rollback" ADD CONSTRAINT "rollback_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/apps/dokploy/drizzle/meta/0093_snapshot.json b/apps/dokploy/drizzle/meta/0093_snapshot.json index 8f047f53..aeb870b5 100644 --- a/apps/dokploy/drizzle/meta/0093_snapshot.json +++ b/apps/dokploy/drizzle/meta/0093_snapshot.json @@ -1,5 +1,5 @@ { - "id": "f8bab14b-cdab-492b-9fe3-b9fc9e56239d", + "id": "7a6a6383-d13c-421b-b186-d49ced92153a", "prevId": "76eb8fe3-21c0-4544-962c-1ae18e8e6730", "version": "7", "dialect": "postgresql", @@ -2184,6 +2184,12 @@ "type": "text", "primaryKey": false, "notNull": false + }, + "rollbackId": { + "name": "rollbackId", + "type": "text", + "primaryKey": false, + "notNull": false } }, "indexes": {}, @@ -2265,6 +2271,19 @@ ], "onDelete": "cascade", "onUpdate": "no action" + }, + "deployment_rollbackId_rollback_rollbackId_fk": { + "name": "deployment_rollbackId_rollback_rollbackId_fk", + "tableFrom": "deployment", + "tableTo": "rollback", + "columnsFrom": [ + "rollbackId" + ], + "columnsTo": [ + "rollbackId" + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -5537,8 +5556,8 @@ "primaryKey": false, "notNull": false }, - "applicationId": { - "name": "applicationId", + "deploymentId": { + "name": "deploymentId", "type": "text", "primaryKey": false, "notNull": true @@ -5564,15 +5583,15 @@ }, "indexes": {}, "foreignKeys": { - "rollback_applicationId_application_applicationId_fk": { - "name": "rollback_applicationId_application_applicationId_fk", + "rollback_deploymentId_deployment_deploymentId_fk": { + "name": "rollback_deploymentId_deployment_deploymentId_fk", "tableFrom": "rollback", - "tableTo": "application", + "tableTo": "deployment", "columnsFrom": [ - "applicationId" + "deploymentId" ], "columnsTo": [ - "applicationId" + "deploymentId" ], "onDelete": "cascade", "onUpdate": "no action" diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json index 9c91ba8e..fff18b2f 100644 --- a/apps/dokploy/drizzle/meta/_journal.json +++ b/apps/dokploy/drizzle/meta/_journal.json @@ -656,8 +656,8 @@ { "idx": 93, "version": "7", - "when": 1748828655755, - "tag": "0093_yielding_typhoid_mary", + "when": 1748835784658, + "tag": "0093_funny_leper_queen", "breakpoints": true } ] diff --git a/apps/dokploy/server/api/routers/deployment.ts b/apps/dokploy/server/api/routers/deployment.ts index d04f6454..129c1e4a 100644 --- a/apps/dokploy/server/api/routers/deployment.ts +++ b/apps/dokploy/server/api/routers/deployment.ts @@ -65,7 +65,11 @@ export const deploymentRouter = createTRPCRouter({ const deploymentsList = await db.query.deployments.findMany({ where: eq(deployments[`${input.type}Id`], input.id), orderBy: desc(deployments.createdAt), + with: { + rollback: true, + }, }); + return deploymentsList; }), }); diff --git a/apps/dokploy/server/api/routers/rollbacks.ts b/apps/dokploy/server/api/routers/rollbacks.ts index b68aa669..6fb1f741 100644 --- a/apps/dokploy/server/api/routers/rollbacks.ts +++ b/apps/dokploy/server/api/routers/rollbacks.ts @@ -1,10 +1,10 @@ import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; -import { apiFindOneRollback, rollbacks } from "@/server/db/schema"; +import { apiFindOneRollback } from "@/server/db/schema"; import { removeRollbackById, rollback } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; -import { eq, desc } from "drizzle-orm"; -import { db } from "@/server/db"; -import { z } from "zod"; +// import { eq, desc } from "drizzle-orm"; +// import { db } from "@/server/db"; +// import { z } from "zod"; export const rollbackRouter = createTRPCRouter({ delete: protectedProcedure @@ -23,26 +23,26 @@ export const rollbackRouter = createTRPCRouter({ }); } }), - all: protectedProcedure - .input( - z.object({ - applicationId: z.string(), - }), - ) - .query(async ({ input }) => { - try { - return await db.query.rollbacks.findMany({ - where: eq(rollbacks.applicationId, input.applicationId), - orderBy: desc(rollbacks.createdAt), - }); - } catch (error) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error input: Fetching rollbacks", - cause: error, - }); - } - }), + // all: protectedProcedure + // .input( + // z.object({ + // applicationId: z.string(), + // }), + // ) + // .query(async ({ input }) => { + // try { + // return await db.query.rollbacks.findMany({ + // where: eq(rollbacks.applicationId, input.applicationId), + // orderBy: desc(rollbacks.createdAt), + // }); + // } catch (error) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: "Error input: Fetching rollbacks", + // cause: error, + // }); + // } + // }), rollback: protectedProcedure .input(apiFindOneRollback) .mutation(async ({ input }) => { diff --git a/apps/dokploy/tailwind.config.ts b/apps/dokploy/tailwind.config.ts index be1f8986..4a7ec8ac 100644 --- a/apps/dokploy/tailwind.config.ts +++ b/apps/dokploy/tailwind.config.ts @@ -15,7 +15,7 @@ const config = { center: true, padding: "2rem", screens: { - "2xl": "1400px", + "2xl": "87.5rem", }, }, extend: { diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index 33c81f8f..099f1da4 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -27,7 +27,6 @@ import { server } from "./server"; import { applicationStatus, certificateType, triggerType } from "./shared"; import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; -import { rollbacks } from "./rollbacks"; export const sourceType = pgEnum("sourceType", [ "docker", "git", @@ -277,7 +276,6 @@ export const applicationsRelations = relations( references: [server.serverId], }), previewDeployments: many(previewDeployments), - rollbacks: many(rollbacks), }), ); diff --git a/packages/server/src/db/schema/deployment.ts b/packages/server/src/db/schema/deployment.ts index 5712d928..1e96806d 100644 --- a/packages/server/src/db/schema/deployment.ts +++ b/packages/server/src/db/schema/deployment.ts @@ -15,6 +15,7 @@ import { compose } from "./compose"; import { previewDeployments } from "./preview-deployments"; import { schedules } from "./schedule"; import { server } from "./server"; +import { rollbacks } from "./rollbacks"; export const deploymentStatus = pgEnum("deploymentStatus", [ "running", "done", @@ -58,6 +59,10 @@ export const deployments = pgTable("deployment", { backupId: text("backupId").references((): AnyPgColumn => backups.backupId, { onDelete: "cascade", }), + rollbackId: text("rollbackId").references( + (): AnyPgColumn => rollbacks.rollbackId, + { onDelete: "cascade" }, + ), }); export const deploymentsRelations = relations(deployments, ({ one }) => ({ @@ -85,6 +90,10 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({ fields: [deployments.backupId], references: [backups.backupId], }), + rollback: one(rollbacks, { + fields: [deployments.deploymentId], + references: [rollbacks.deploymentId], + }), })); const schema = createInsertSchema(deployments, { diff --git a/packages/server/src/db/schema/rollbacks.ts b/packages/server/src/db/schema/rollbacks.ts index 6775c72c..a91262b0 100644 --- a/packages/server/src/db/schema/rollbacks.ts +++ b/packages/server/src/db/schema/rollbacks.ts @@ -3,7 +3,7 @@ import { pgTable, serial, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { applications } from "./application"; +import { deployments } from "./deployment"; export const rollbacks = pgTable("rollback", { rollbackId: text("rollbackId") @@ -11,9 +11,9 @@ export const rollbacks = pgTable("rollback", { .primaryKey() .$defaultFn(() => nanoid()), env: text("env"), - applicationId: text("applicationId") + deploymentId: text("deploymentId") .notNull() - .references(() => applications.applicationId, { + .references(() => deployments.deploymentId, { onDelete: "cascade", }), version: serial(), @@ -26,9 +26,9 @@ export const rollbacks = pgTable("rollback", { export type Rollback = typeof rollbacks.$inferSelect; export const rollbacksRelations = relations(rollbacks, ({ one }) => ({ - application: one(applications, { - fields: [rollbacks.applicationId], - references: [applications.applicationId], + deployment: one(deployments, { + fields: [rollbacks.deploymentId], + references: [deployments.deploymentId], }), })); diff --git a/packages/server/src/services/application.ts b/packages/server/src/services/application.ts index 7a690db1..a4661eb9 100644 --- a/packages/server/src/services/application.ts +++ b/packages/server/src/services/application.ts @@ -224,12 +224,10 @@ export const deployApplication = async ({ application.project.env, ); - console.log(resolveEnvs); - await createRollback({ appName: application.appName, env: resolveEnvs.join("\n"), - applicationId: applicationId, + deploymentId: deployment.deploymentId, }); } diff --git a/packages/server/src/services/deployment.ts b/packages/server/src/services/deployment.ts index 6a76fcaa..d74a1e26 100644 --- a/packages/server/src/services/deployment.ts +++ b/packages/server/src/services/deployment.ts @@ -34,20 +34,34 @@ import { findScheduleById } from "./schedule"; export type Deployment = typeof deployments.$inferSelect; -export const findDeploymentById = async (applicationId: string) => { - const application = await db.query.deployments.findFirst({ - where: eq(deployments.applicationId, applicationId), +export const findDeploymentById = async (deploymentId: string) => { + const deployment = await db.query.deployments.findFirst({ + where: eq(deployments.deploymentId, deploymentId), with: { application: true, }, }); - if (!application) { + if (!deployment) { throw new TRPCError({ code: "NOT_FOUND", message: "Deployment not found", }); } - return application; + return deployment; +}; + +export const findDeploymentByApplicationId = async (applicationId: string) => { + const deployment = await db.query.deployments.findFirst({ + where: eq(deployments.applicationId, applicationId), + }); + + if (!deployment) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Deployment not found", + }); + } + return deployment; }; export const createDeployment = async ( diff --git a/packages/server/src/services/rollbacks.ts b/packages/server/src/services/rollbacks.ts index f70f83f0..e32e5ba0 100644 --- a/packages/server/src/services/rollbacks.ts +++ b/packages/server/src/services/rollbacks.ts @@ -7,6 +7,7 @@ import { getRemoteDocker } from "../utils/servers/remote-docker"; import type { ApplicationNested } from "../utils/builders"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import type { CreateServiceOptions } from "dockerode"; +import { findDeploymentById } from "./deployment"; export const createRollback = async ( input: z.infer, @@ -31,7 +32,13 @@ export const createRollback = async ( }) .where(eq(rollbacks.rollbackId, rollback.rollbackId)); - const application = await findApplicationById(input.applicationId); + const deployment = await findDeploymentById(rollback.deploymentId); + + if (!deployment?.applicationId) { + throw new Error("Deployment not found"); + } + + const application = await findApplicationById(deployment.applicationId); await createRollbackImage(application, tagImage); @@ -86,7 +93,13 @@ export const removeRollbackById = async (rollbackId: string) => { if (result?.image) { try { - const application = await findApplicationById(result.applicationId); + const deployment = await findDeploymentById(result.deploymentId); + + if (!deployment?.applicationId) { + throw new Error("Deployment not found"); + } + + const application = await findApplicationById(deployment.applicationId); await deleteRollbackImage(result.image, application.serverId); } catch (error) { console.error(error); @@ -99,7 +112,13 @@ export const removeRollbackById = async (rollbackId: string) => { export const rollback = async (rollbackId: string) => { const result = await findRollbackById(rollbackId); - const application = await findApplicationById(result.applicationId); + const deployment = await findDeploymentById(result.deploymentId); + + if (!deployment?.applicationId) { + throw new Error("Deployment not found"); + } + + const application = await findApplicationById(deployment.applicationId); await rollbackApplication( application.appName, @@ -122,7 +141,7 @@ const rollbackApplication = async ( TaskTemplate: { ContainerSpec: { Image: image, - Env: env.split("\n"), + // Env: env.split("\n"), }, }, };