Merge branch 'Dokploy:canary' into canary

This commit is contained in:
фырат ёздэн
2025-11-19 18:49:38 +03:00
committed by GitHub
16 changed files with 257 additions and 26 deletions

View File

@@ -0,0 +1,65 @@
import { Scissors } 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 KillBuild = ({ id, type }: Props) => {
const { mutateAsync, isLoading } =
type === "application"
? api.application.killBuild.useMutation()
: api.compose.killBuild.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="w-fit" isLoading={isLoading}>
Kill Build
<Scissors className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure to kill the build?</AlertDialogTitle>
<AlertDialogDescription>
This will kill the build process
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId: id || "",
composeId: id || "",
})
.then(() => {
toast.success("Build killed successfully");
})
.catch((err) => {
toast.error(err.message);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -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 { KillBuild } from "./kill-build";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
@@ -143,6 +144,9 @@ export const ShowDeployments = ({
</CardDescription>
</div>
<div className="flex flex-row items-center gap-2">
{(type === "application" || type === "compose") && (
<KillBuild id={id} type={type} />
)}
{(type === "application" || type === "compose") && (
<CancelQueues id={id} type={type} />
)}

View File

@@ -182,7 +182,16 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
id={deployment.previewDeploymentId}
type="previewDeployment"
serverId={data?.serverId || ""}
/>
>
<Button
variant="outline"
size="sm"
className="gap-2"
>
<RocketIcon className="size-4" />
Deployments
</Button>
</ShowDeploymentsModal>
<AddPreviewDomain
previewDeploymentId={`${deployment.previewDeploymentId}`}

View File

@@ -86,7 +86,7 @@ export const ShowVolumeBackups = ({
</CardTitle>
<CardDescription>
Schedule volume backups to run automatically at specified
intervals.
intervals
</CardDescription>
</div>
<div className="flex items-center gap-2 flex-wrap">

View File

@@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import {
AlertDialog,
@@ -44,7 +45,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
import {
Select,
SelectContent,
@@ -52,12 +52,14 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TimeBadge } from "@/components/ui/time-badge";
import { api } from "@/utils/api";
import { HandleProject } from "./handle-project";
import { ProjectEnvironment } from "./project-environment";
export const ShowProjects = () => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data, isLoading } = api.project.all.useQuery();
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
@@ -135,6 +137,11 @@ export const ShowProjects = () => {
<BreadcrumbSidebar
list={[{ name: "Projects", href: "/dashboard/projects" }]}
/>
{!isCloud && (
<div className="absolute top-5 right-5">
<TimeBadge />
</div>
)}
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
<div className="rounded-xl bg-background shadow-md ">
@@ -148,7 +155,6 @@ export const ShowProjects = () => {
Create and manage your projects
</CardDescription>
</CardHeader>
{(auth?.role === "owner" || auth?.canCreateProjects) && (
<div className="">
<HandleProject />
@@ -298,7 +304,13 @@ export const ShowProjects = () => {
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
>
<span className="truncate">
{domain.host}
@@ -340,7 +352,13 @@ export const ShowProjects = () => {
<Link
className="space-x-4 text-xs cursor-pointer justify-between"
target="_blank"
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
href={`${
domain.https
? "https"
: "http"
}://${domain.host}${
domain.path
}`}
>
<span className="truncate">
{domain.host}

View File

@@ -83,6 +83,7 @@ import { AddOrganization } from "../dashboard/organization/handle-organization";
import { DialogAction } from "../shared/dialog-action";
import { Logo } from "../shared/logo";
import { Button } from "../ui/button";
import { TimeBadge } from "../ui/time-badge";
import { UpdateServerButton } from "./update-server";
import { UserNav } from "./user-nav";
@@ -1125,6 +1126,7 @@ export default function Page({ children }: Props) {
</BreadcrumbList>
</Breadcrumb>
</div>
{!isCloud && <TimeBadge />}
</div>
</header>
)}

View File

@@ -0,0 +1,58 @@
"use client";
import { useEffect, useState } from "react";
import { api } from "@/utils/api";
export function TimeBadge() {
const { data: serverTime } = api.server.getServerTime.useQuery(undefined);
const [time, setTime] = useState<Date | null>(null);
useEffect(() => {
if (serverTime?.time) {
setTime(new Date(serverTime.time));
}
}, [serverTime]);
useEffect(() => {
const timer = setInterval(() => {
setTime((prevTime) => {
if (!prevTime) return null;
const newTime = new Date(prevTime.getTime() + 1000);
return newTime;
});
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
if (!time || !serverTime?.timezone) {
return null;
}
const getUtcOffset = (timeZone: string) => {
const date = new Date();
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
const sign = offset >= 0 ? "+" : "-";
const hours = Math.floor(Math.abs(offset));
const minutes = (Math.abs(offset) * 60) % 60;
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}`;
};
return (
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
<span className="hidden sm:inline">Server Time:</span>
<span className="font-medium tabular-nums">
{time.toLocaleTimeString()}
</span>
<span className="hidden sm:inline text-muted-foreground">
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
</span>
</div>
);
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.25.6",
"version": "v0.25.8",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -58,7 +58,11 @@ import {
applications,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import {
cleanQueuesByApplication,
killDockerBuild,
myQueue,
} from "@/server/queues/queueSetup";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
@@ -725,7 +729,21 @@ export const applicationRouter = createTRPCRouter({
}
await cleanQueuesByApplication(input.applicationId);
}),
killBuild: 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 kill this build",
});
}
await killDockerBuild("application", application.serverId);
}),
readTraefikConfig: protectedProcedure
.input(apiFindOneApplication)
.query(async ({ input, ctx }) => {

View File

@@ -59,7 +59,11 @@ import {
compose as composeTable,
} from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import {
cleanQueuesByCompose,
killDockerBuild,
myQueue,
} from "@/server/queues/queueSetup";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
@@ -248,6 +252,21 @@ export const composeRouter = createTRPCRouter({
await cleanQueuesByCompose(input.composeId);
return { success: true, message: "Queues cleaned successfully" };
}),
killBuild: 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 kill this build",
});
}
await killDockerBuild("compose", compose.serverId);
}),
loadServices: protectedProcedure
.input(apiFetchServices)

View File

@@ -383,6 +383,15 @@ export const serverRouter = createTRPCRouter({
const ip = await getPublicIpWithFallback();
return ip;
}),
getServerTime: protectedProcedure.query(() => {
if (IS_CLOUD) {
return null;
}
return {
time: new Date(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
}),
getServerMetrics: protectedProcedure
.input(
z.object({

View File

@@ -1,3 +1,7 @@
import {
execAsync,
execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync";
import { Queue } from "bullmq";
import { redisConfig } from "./redis-connection";
@@ -41,4 +45,31 @@ export const cleanQueuesByCompose = async (composeId: string) => {
}
};
export const killDockerBuild = async (
type: "application" | "compose",
serverId: string | null,
) => {
try {
if (type === "application") {
const command = `pkill -2 -f "docker build"`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
} else if (type === "compose") {
const command = `pkill -2 -f "docker compose"`;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
}
} catch (error) {
console.error(error);
}
};
export { myQueue };

View File

@@ -59,7 +59,8 @@ export const getUpdateData = async (): Promise<IUpdateData> => {
let currentDigest: string;
try {
currentDigest = await getServiceImageDigest();
} catch {
} catch (error) {
console.error(error);
// Docker service might not exist locally
// You can run the # Installation command for docker service create mentioned in the below docs to test it locally:
// https://docs.dokploy.com/docs/core/manual-installation

View File

@@ -2,7 +2,7 @@ import { dirname, join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { InferResultType } from "@dokploy/server/types/with";
import boxen from "boxen";
import { writeDomainsToComposeRemote } from "../docker/domain";
import { writeDomainsToCompose } from "../docker/domain";
import {
encodeBase64,
getEnviromentVariablesObject,
@@ -22,7 +22,7 @@ export const getBuildComposeCommand = async (compose: ComposeNested) => {
const projectPath = join(COMPOSE_PATH, compose.appName, "code");
const exportEnvCommand = getExportEnvCommand(compose);
const newCompose = await writeDomainsToComposeRemote(compose, domains);
const newCompose = await writeDomainsToCompose(compose, domains);
const logContent = `
App Name: ${appName}
Build Compose 🐳

View File

@@ -102,7 +102,7 @@ export const readComposeFile = async (compose: Compose) => {
return null;
};
export const writeDomainsToComposeRemote = async (
export const writeDomainsToCompose = async (
compose: Compose,
domains: Domain[],
) => {
@@ -120,19 +120,16 @@ echo "❌ Error: Compose file not found";
exit 1;
`;
}
if (compose.serverId) {
const composeString = stringify(composeConverted, { lineWidth: 1000 });
const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`;
}
const composeString = stringify(composeConverted, { lineWidth: 1000 });
const encodedContent = encodeBase64(composeString);
return `echo "${encodedContent}" | base64 -d > "${path}";`;
} catch (error) {
// @ts-ignore
return `echo "❌ Has occured an error: ${error?.message || error}";
return `echo "❌ Has occurred an error: ${error?.message || error}";
exit 1;
`;
}
return "";
};
export const addDomainToCompose = async (
compose: Compose,

View File

@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
database: string,
databaseUser: string,
) => {
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`;
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`;
};
export const getMariadbRestoreCommand = (
@@ -15,14 +15,14 @@ export const getMariadbRestoreCommand = (
databaseUser: string,
databasePassword: string,
) => {
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`;
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`;
};
export const getMysqlRestoreCommand = (
database: string,
databasePassword: string,
) => {
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`;
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`;
};
export const getMongoRestoreCommand = (
@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
databaseUser: string,
databasePassword: string,
) => {
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive"`;
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
};
export const getComposeSearchCommand = (