feat/fix: fixed stop button & added start button

This commit is contained in:
190km
2024-12-03 02:20:20 +01:00
parent 63998f71ec
commit fd0a472468
6 changed files with 295 additions and 116 deletions

View File

@@ -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} />
)} )}

View 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>
);
};

View 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>
);
};

View File

@@ -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 });
} }
} }

View File

@@ -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

View File

@@ -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 {