mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat/fix: fixed stop button & added start button
This commit is contained in:
@@ -14,6 +14,7 @@ import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
import { StartCompose } from "../start-compose";
|
||||||
import { DeployCompose } from "./deploy-compose";
|
import { DeployCompose } from "./deploy-compose";
|
||||||
import { RedbuildCompose } from "./rebuild-compose";
|
import { RedbuildCompose } from "./rebuild-compose";
|
||||||
import { StopCompose } from "./stop-compose";
|
import { StopCompose } from "./stop-compose";
|
||||||
@@ -71,7 +72,10 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
Autodeploy {data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
Autodeploy {data?.autoDeploy && <CheckCircle2 className="size-4" />}
|
||||||
</Toggle>
|
</Toggle>
|
||||||
<RedbuildCompose composeId={composeId} />
|
<RedbuildCompose composeId={composeId} />
|
||||||
{data?.composeType === "docker-compose" && (
|
{data?.composeType === "docker-compose" &&
|
||||||
|
data?.composeStatus === "idle" ? (
|
||||||
|
<StartCompose composeId={composeId} />
|
||||||
|
) : (
|
||||||
<StopCompose composeId={composeId} />
|
<StopCompose composeId={composeId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
65
apps/dokploy/components/dashboard/compose/start-compose.tsx
Normal file
65
apps/dokploy/components/dashboard/compose/start-compose.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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";
|
||||||
|
import { CheckCircle2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StartCompose = ({ composeId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.compose.start.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="secondary" isLoading={isLoading}>
|
||||||
|
Start
|
||||||
|
<CheckCircle2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure to start the compose?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will start the compose
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.compose.one.invalidate({
|
||||||
|
composeId,
|
||||||
|
});
|
||||||
|
toast.success("Compose started succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to start the Compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
65
apps/dokploy/components/dashboard/compose/stop-compose.tsx
Normal file
65
apps/dokploy/components/dashboard/compose/stop-compose.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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";
|
||||||
|
import { Ban } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StopCompose = ({ composeId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" isLoading={isLoading}>
|
||||||
|
Stop
|
||||||
|
<Ban className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you absolutely sure to stop the compose?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will stop the compose
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.compose.one.invalidate({
|
||||||
|
composeId,
|
||||||
|
});
|
||||||
|
toast.success("Compose stopped succesfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to stop the Compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,138 +10,138 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const signature = req.headers["x-hub-signature-256"];
|
const signature = req.headers["x-hub-signature-256"];
|
||||||
const githubBody = req.body;
|
const githubBody = req.body;
|
||||||
|
|
||||||
if (!githubBody?.installation?.id) {
|
if (!githubBody?.installation?.id) {
|
||||||
res.status(400).json({ message: "Github Installation not found" });
|
res.status(400).json({ message: "Github Installation not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const githubResult = await db.query.github.findFirst({
|
const githubResult = await db.query.github.findFirst({
|
||||||
where: eq(github.githubInstallationId, githubBody.installation.id),
|
where: eq(github.githubInstallationId, githubBody.installation.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!githubResult) {
|
if (!githubResult) {
|
||||||
res.status(400).json({ message: "Github Installation not found" });
|
res.status(400).json({ message: "Github Installation not found" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!githubResult.githubWebhookSecret) {
|
if (!githubResult.githubWebhookSecret) {
|
||||||
res.status(400).json({ message: "Github Webhook Secret not set" });
|
res.status(400).json({ message: "Github Webhook Secret not set" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const webhooks = new Webhooks({
|
const webhooks = new Webhooks({
|
||||||
secret: githubResult.githubWebhookSecret,
|
secret: githubResult.githubWebhookSecret,
|
||||||
});
|
});
|
||||||
|
|
||||||
const verified = await webhooks.verify(
|
const verified = await webhooks.verify(
|
||||||
JSON.stringify(githubBody),
|
JSON.stringify(githubBody),
|
||||||
signature as string
|
signature as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
res.status(401).json({ message: "Unauthorized" });
|
res.status(401).json({ message: "Unauthorized" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers["x-github-event"] === "ping") {
|
if (req.headers["x-github-event"] === "ping") {
|
||||||
res.status(200).json({ message: "Ping received, webhook is active" });
|
res.status(200).json({ message: "Ping received, webhook is active" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers["x-github-event"] !== "push") {
|
if (req.headers["x-github-event"] !== "push") {
|
||||||
res.status(400).json({ message: "We only accept push events" });
|
res.status(400).json({ message: "We only accept push events" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
const branchName = githubBody?.ref?.replace("refs/heads/", "");
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(applications.sourceType, "github"),
|
eq(applications.sourceType, "github"),
|
||||||
eq(applications.autoDeploy, true),
|
eq(applications.autoDeploy, true),
|
||||||
eq(applications.branch, branchName),
|
eq(applications.branch, branchName),
|
||||||
eq(applications.repository, repository)
|
eq(applications.repository, repository),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
applicationId: app.applicationId as string,
|
applicationId: app.applicationId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "application",
|
applicationType: "application",
|
||||||
server: !!app.serverId,
|
server: !!app.serverId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_CLOUD && app.serverId) {
|
if (IS_CLOUD && app.serverId) {
|
||||||
jobData.serverId = app.serverId;
|
jobData.serverId = app.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
{ ...jobData },
|
{ ...jobData },
|
||||||
{
|
{
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const composeApps = await db.query.compose.findMany({
|
const composeApps = await db.query.compose.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(compose.sourceType, "github"),
|
eq(compose.sourceType, "github"),
|
||||||
eq(compose.autoDeploy, true),
|
eq(compose.autoDeploy, true),
|
||||||
eq(compose.branch, branchName),
|
eq(compose.branch, branchName),
|
||||||
eq(compose.repository, repository)
|
eq(compose.repository, repository),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const composeApp of composeApps) {
|
for (const composeApp of composeApps) {
|
||||||
const jobData: DeploymentJob = {
|
const jobData: DeploymentJob = {
|
||||||
composeId: composeApp.composeId as string,
|
composeId: composeApp.composeId as string,
|
||||||
titleLog: deploymentTitle,
|
titleLog: deploymentTitle,
|
||||||
type: "deploy",
|
type: "deploy",
|
||||||
applicationType: "compose",
|
applicationType: "compose",
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
server: !!composeApp.serverId,
|
server: !!composeApp.serverId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IS_CLOUD && composeApp.serverId) {
|
if (IS_CLOUD && composeApp.serverId) {
|
||||||
jobData.serverId = composeApp.serverId;
|
jobData.serverId = composeApp.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
{ ...jobData },
|
{ ...jobData },
|
||||||
{
|
{
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalApps = apps.length + composeApps.length;
|
const totalApps = apps.length + composeApps.length;
|
||||||
const emptyApps = totalApps === 0;
|
const emptyApps = totalApps === 0;
|
||||||
|
|
||||||
if (emptyApps) {
|
if (emptyApps) {
|
||||||
res.status(200).json({ message: "No apps to deploy" });
|
res.status(200).json({ message: "No apps to deploy" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({ message: "Error To Deploy Application", error });
|
res.status(400).json({ message: "Error To Deploy Application", error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
removeCompose,
|
removeCompose,
|
||||||
removeComposeDirectory,
|
removeComposeDirectory,
|
||||||
removeDeploymentsByComposeId,
|
removeDeploymentsByComposeId,
|
||||||
|
startCompose,
|
||||||
stopCompose,
|
stopCompose,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
@@ -309,6 +310,20 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
await stopCompose(input.composeId);
|
await stopCompose(input.composeId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
start: protectedProcedure
|
||||||
|
.input(apiFindCompose)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const compose = await findComposeById(input.composeId);
|
||||||
|
if (compose.project.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to stop this compose",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await startCompose(input.composeId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
getDefaultCommand: protectedProcedure
|
getDefaultCommand: protectedProcedure
|
||||||
|
|||||||
@@ -463,6 +463,36 @@ export const removeCompose = async (compose: Compose) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const startCompose = async (composeId: string) => {
|
||||||
|
const compose = await findComposeById(composeId);
|
||||||
|
try {
|
||||||
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
|
if (compose.composeType === "docker-compose") {
|
||||||
|
if (compose.serverId) {
|
||||||
|
await execAsyncRemote(
|
||||||
|
compose.serverId,
|
||||||
|
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await execAsync(`docker compose -p ${compose.appName} up -d`, {
|
||||||
|
cwd: join(COMPOSE_PATH, compose.appName, "code"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateCompose(composeId, {
|
||||||
|
composeStatus: "done",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await updateCompose(composeId, {
|
||||||
|
composeStatus: "idle",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const stopCompose = async (composeId: string) => {
|
export const stopCompose = async (composeId: string) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user