mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-12 06:55:19 +00: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 { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -35,7 +35,7 @@ export const CancelQueues = ({ id, type }: Props) => {
|
|||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
|
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
|
||||||
Cancel Queues
|
Cancel Queues
|
||||||
<Paintbrush className="size-4" />
|
<Ban className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<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 { api, type RouterOutputs } from "@/utils/api";
|
||||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||||
import { CancelQueues } from "./cancel-queues";
|
import { CancelQueues } from "./cancel-queues";
|
||||||
|
import { ClearDeployments } from "./clear-deployments";
|
||||||
import { KillBuild } from "./kill-build";
|
import { KillBuild } from "./kill-build";
|
||||||
import { RefreshToken } from "./refresh-token";
|
import { RefreshToken } from "./refresh-token";
|
||||||
import { ShowDeployment } from "./show-deployment";
|
import { ShowDeployment } from "./show-deployment";
|
||||||
@@ -144,6 +145,9 @@ export const ShowDeployments = ({
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center flex-wrap gap-2">
|
<div className="flex flex-row items-center flex-wrap gap-2">
|
||||||
|
{(type === "application" || type === "compose") && (
|
||||||
|
<ClearDeployments id={id} type={type} />
|
||||||
|
)}
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<KillBuild id={id} type={type} />
|
<KillBuild id={id} type={type} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
|
clearOldDeploymentsByApplicationId,
|
||||||
createApplication,
|
createApplication,
|
||||||
deleteAllMiddlewares,
|
deleteAllMiddlewares,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
@@ -734,6 +735,26 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
await cleanQueuesByApplication(input.applicationId);
|
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
|
killBuild: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
addDomainToCompose,
|
addDomainToCompose,
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
|
clearOldDeploymentsByComposeId,
|
||||||
cloneCompose,
|
cloneCompose,
|
||||||
createCommand,
|
createCommand,
|
||||||
createCompose,
|
createCompose,
|
||||||
@@ -252,6 +253,26 @@ export const composeRouter = createTRPCRouter({
|
|||||||
await cleanQueuesByCompose(input.composeId);
|
await cleanQueuesByCompose(input.composeId);
|
||||||
return { success: true, message: "Queues cleaned successfully" };
|
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
|
killBuild: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -831,3 +831,111 @@ export const findAllDeploymentsByServerId = async (serverId: string) => {
|
|||||||
});
|
});
|
||||||
return deploymentsList;
|
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