mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
feat: add ability to delete old deployments
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Paintbrush } from "lucide-react";
|
||||
import { Ban } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -35,7 +35,7 @@ export const CancelQueues = ({ id, type }: Props) => {
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
|
||||
Cancel Queues
|
||||
<Paintbrush className="size-4" />
|
||||
<Ban className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Paintbrush } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "application" | "compose";
|
||||
}
|
||||
|
||||
export const ClearDeployments = ({ id, type }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, isLoading } =
|
||||
type === "application"
|
||||
? api.application.clearDeployments.useMutation()
|
||||
: api.compose.clearDeployments.useMutation();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" className="w-fit" isLoading={isLoading}>
|
||||
Clear deployments
|
||||
<Paintbrush className="size-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you sure you want to clear old deployments?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will delete all old deployment records and logs, keeping only the active deployment (the most recent successful one).
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
applicationId: id || "",
|
||||
composeId: id || "",
|
||||
})
|
||||
.then(async (result) => {
|
||||
toast.success(`${result.deletedCount} old deployments cleared successfully`);
|
||||
// Invalidate deployment queries to refresh the list
|
||||
await utils.deployment.allByType.invalidate({
|
||||
id,
|
||||
type,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||
import { CancelQueues } from "./cancel-queues";
|
||||
import { ClearDeployments } from "./clear-deployments";
|
||||
import { KillBuild } from "./kill-build";
|
||||
import { RefreshToken } from "./refresh-token";
|
||||
import { ShowDeployment } from "./show-deployment";
|
||||
@@ -144,6 +145,9 @@ export const ShowDeployments = ({
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-row items-center flex-wrap gap-2">
|
||||
{(type === "application" || type === "compose") && (
|
||||
<ClearDeployments id={id} type={type} />
|
||||
)}
|
||||
{(type === "application" || type === "compose") && (
|
||||
<KillBuild id={id} type={type} />
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
clearOldDeploymentsByApplicationId,
|
||||
createApplication,
|
||||
deleteAllMiddlewares,
|
||||
findApplicationById,
|
||||
@@ -734,6 +735,26 @@ export const applicationRouter = createTRPCRouter({
|
||||
}
|
||||
await cleanQueuesByApplication(input.applicationId);
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clear deployments for this application",
|
||||
});
|
||||
}
|
||||
const result = await clearOldDeploymentsByApplicationId(input.applicationId);
|
||||
return {
|
||||
success: true,
|
||||
message: `${result.deletedCount} old deployments cleared successfully`,
|
||||
deletedCount: result.deletedCount,
|
||||
};
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
addDomainToCompose,
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
clearOldDeploymentsByComposeId,
|
||||
cloneCompose,
|
||||
createCommand,
|
||||
createCompose,
|
||||
@@ -252,6 +253,26 @@ export const composeRouter = createTRPCRouter({
|
||||
await cleanQueuesByCompose(input.composeId);
|
||||
return { success: true, message: "Queues cleaned successfully" };
|
||||
}),
|
||||
clearDeployments: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (
|
||||
compose.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to clear deployments for this compose",
|
||||
});
|
||||
}
|
||||
const result = await clearOldDeploymentsByComposeId(input.composeId);
|
||||
return {
|
||||
success: true,
|
||||
message: `${result.deletedCount} old deployments cleared successfully`,
|
||||
deletedCount: result.deletedCount,
|
||||
};
|
||||
}),
|
||||
killBuild: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -831,3 +831,111 @@ export const findAllDeploymentsByServerId = async (serverId: string) => {
|
||||
});
|
||||
return deploymentsList;
|
||||
};
|
||||
|
||||
export const clearOldDeploymentsByApplicationId = async (
|
||||
applicationId: string,
|
||||
) => {
|
||||
// Get all deployments ordered by creation date (newest first)
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
|
||||
// Find the most recent successful deployment (status "done")
|
||||
const activeDeployment = deploymentsList.find(
|
||||
(deployment) => deployment.status === "done",
|
||||
);
|
||||
|
||||
// If there's an active deployment, keep it and remove all others
|
||||
// If there's no active deployment, keep the most recent one and remove the rest
|
||||
let deploymentsToKeep: string[] = [];
|
||||
|
||||
if (activeDeployment) {
|
||||
deploymentsToKeep.push(activeDeployment.deploymentId);
|
||||
} else if (deploymentsList.length > 0) {
|
||||
// Keep the most recent deployment even if it's not "done"
|
||||
deploymentsToKeep.push(deploymentsList[0]!.deploymentId);
|
||||
}
|
||||
|
||||
const deploymentsToDelete = deploymentsList.filter(
|
||||
(deployment) => !deploymentsToKeep.includes(deployment.deploymentId),
|
||||
);
|
||||
|
||||
// Delete old deployments and their log files
|
||||
for (const deployment of deploymentsToDelete) {
|
||||
if (deployment.rollbackId) {
|
||||
await removeRollbackById(deployment.rollbackId);
|
||||
}
|
||||
|
||||
// Remove log file if it exists
|
||||
const logPath = deployment.logPath;
|
||||
if (logPath && logPath !== "." && existsSync(logPath)) {
|
||||
try {
|
||||
await fsPromises.unlink(logPath);
|
||||
} catch (error) {
|
||||
console.error(`Error removing log file ${logPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deployment from database
|
||||
await removeDeployment(deployment.deploymentId);
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCount: deploymentsToDelete.length,
|
||||
keptDeployment: deploymentsToKeep[0] || null,
|
||||
};
|
||||
};
|
||||
|
||||
export const clearOldDeploymentsByComposeId = async (composeId: string) => {
|
||||
// Get all deployments ordered by creation date (newest first)
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments.composeId, composeId),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
});
|
||||
|
||||
// Find the most recent successful deployment (status "done")
|
||||
const activeDeployment = deploymentsList.find(
|
||||
(deployment) => deployment.status === "done",
|
||||
);
|
||||
|
||||
// If there's an active deployment, keep it and remove all others
|
||||
// If there's no active deployment, keep the most recent one and remove the rest
|
||||
let deploymentsToKeep: string[] = [];
|
||||
|
||||
if (activeDeployment) {
|
||||
deploymentsToKeep.push(activeDeployment.deploymentId);
|
||||
} else if (deploymentsList.length > 0) {
|
||||
// Keep the most recent deployment even if it's not "done"
|
||||
deploymentsToKeep.push(deploymentsList[0]!.deploymentId);
|
||||
}
|
||||
|
||||
const deploymentsToDelete = deploymentsList.filter(
|
||||
(deployment) => !deploymentsToKeep.includes(deployment.deploymentId),
|
||||
);
|
||||
|
||||
// Delete old deployments and their log files
|
||||
for (const deployment of deploymentsToDelete) {
|
||||
if (deployment.rollbackId) {
|
||||
await removeRollbackById(deployment.rollbackId);
|
||||
}
|
||||
|
||||
// Remove log file if it exists
|
||||
const logPath = deployment.logPath;
|
||||
if (logPath && logPath !== "." && existsSync(logPath)) {
|
||||
try {
|
||||
await fsPromises.unlink(logPath);
|
||||
} catch (error) {
|
||||
console.error(`Error removing log file ${logPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deployment from database
|
||||
await removeDeployment(deployment.deploymentId);
|
||||
}
|
||||
|
||||
return {
|
||||
deletedCount: deploymentsToDelete.length,
|
||||
keptDeployment: deploymentsToKeep[0] || null,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user