Merge branch 'canary' into patches-impl

This commit is contained in:
Mauricio Siu
2026-02-16 23:52:26 -06:00
83 changed files with 18938 additions and 3822 deletions

View File

@@ -105,7 +105,14 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
const modeData =
formData.type === "Replicated"
? { Replicated: { Replicas: formData.Replicas } }
? {
Replicated: {
Replicas:
formData.Replicas !== undefined && formData.Replicas !== ""
? Number(formData.Replicas)
: undefined,
},
}
: { Global: {} };
await mutateAsync({

View File

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

View File

@@ -0,0 +1,73 @@
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();
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 () => {
toast.success("Old deployments cleared successfully");
await utils.deployment.allByType.invalidate({
id,
type: type as "application" | "compose",
});
})
.catch((err) => {
toast.error(err.message);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -6,6 +6,7 @@ import {
RefreshCcw,
RocketIcon,
Settings,
Trash2,
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
@@ -25,6 +26,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";
@@ -77,6 +79,8 @@ export const ShowDeployments = ({
api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
api.deployment.killProcess.useMutation();
const { mutateAsync: removeDeployment, isLoading: isRemovingDeployment } =
api.deployment.removeDeployment.useMutation();
// Cancel deployment mutations
const {
@@ -144,6 +148,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} />
)}
@@ -252,6 +259,8 @@ export const ShowDeployments = ({
const isExpanded = expandedDescriptions.has(
deployment.deploymentId,
);
const canDelete =
deployment.status === "done" || deployment.status === "error";
return (
<div
@@ -370,6 +379,33 @@ export const ShowDeployments = ({
View
</Button>
{canDelete && (
<DialogAction
title="Delete Deployment"
description="Are you sure you want to delete this deployment? This action cannot be undone."
type="default"
onClick={async () => {
try {
await removeDeployment({
deploymentId: deployment.deploymentId,
});
toast.success("Deployment deleted successfully");
} catch (error) {
toast.error("Error deleting deployment");
}
}}
>
<Button
variant="destructive"
size="sm"
isLoading={isRemovingDeployment}
>
Delete
<Trash2 className="size-4" />
</Button>
</DialogAction>
)}
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (

View File

@@ -18,6 +18,7 @@ import {
PushoverIcon,
ResendIcon,
SlackIcon,
TeamsIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
import { Button } from "@/components/ui/button";
@@ -164,6 +165,12 @@ export const notificationSchema = z.discriminatedUnion("type", [
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("teams"),
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
})
.merge(notificationBaseSchema),
]);
export const notificationsMap = {
@@ -183,6 +190,10 @@ export const notificationsMap = {
icon: <LarkIcon className="text-muted-foreground" />,
label: "Lark",
},
teams: {
icon: <TeamsIcon className="text-muted-foreground" />,
label: "Microsoft Teams",
},
email: {
icon: <Mail size={29} className="text-muted-foreground" />,
label: "Email",
@@ -244,6 +255,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
api.notification.testNtfyConnection.useMutation();
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
api.notification.testLarkConnection.useMutation();
const { mutateAsync: testTeamsConnection, isLoading: isLoadingTeams } =
api.notification.testTeamsConnection.useMutation();
const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } =
api.notification.testCustomConnection.useMutation();
@@ -278,6 +291,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const larkMutation = notificationId
? api.notification.updateLark.useMutation()
: api.notification.createLark.useMutation();
const teamsMutation = notificationId
? api.notification.updateTeams.useMutation()
: api.notification.createTeams.useMutation();
const pushoverMutation = notificationId
? api.notification.updatePushover.useMutation()
: api.notification.createPushover.useMutation();
@@ -353,7 +369,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
webhookUrl: notification.discord?.webhookUrl,
decoration: notification.discord?.decoration || undefined,
decoration: notification.discord?.decoration ?? undefined,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
@@ -400,7 +416,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
appToken: notification.gotify?.appToken,
decoration: notification.gotify?.decoration || undefined,
decoration: notification.gotify?.decoration ?? undefined,
priority: notification.gotify?.priority,
serverUrl: notification.gotify?.serverUrl,
name: notification.name,
@@ -435,6 +451,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
volumeBackup: notification.volumeBackup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "teams") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
webhookUrl: notification.teams?.webhookUrl,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "custom") {
form.reset({
appBuildError: notification.appBuildError,
@@ -488,6 +517,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
gotify: gotifyMutation,
ntfy: ntfyMutation,
lark: larkMutation,
teams: teamsMutation,
custom: customMutation,
pushover: pushoverMutation,
};
@@ -630,6 +660,20 @@ export const HandleNotifications = ({ notificationId }: Props) => {
larkId: notification?.larkId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "teams") {
promise = teamsMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
name: data.name,
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
teamsId: notification?.teamsId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "custom") {
// Convert headers array to object
const headersRecord =
@@ -1465,6 +1509,32 @@ export const HandleNotifications = ({ notificationId }: Props) => {
/>
</>
)}
{type === "teams" && (
<>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://xxx.webhook.office.com/webhookb2/..."
{...field}
/>
</FormControl>
<FormDescription>
Incoming Webhook URL from a Teams channel. Add an
Incoming Webhook in your channel settings to get the
URL.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{type === "pushover" && (
<>
<FormField
@@ -1780,6 +1850,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingGotify ||
isLoadingNtfy ||
isLoadingLark ||
isLoadingTeams ||
isLoadingCustom ||
isLoadingPushover
}
@@ -1841,6 +1912,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
await testLarkConnection({
webhookUrl: data.webhookUrl,
});
} else if (data.type === "teams") {
await testTeamsConnection({
webhookUrl: data.webhookUrl,
});
} else if (data.type === "custom") {
const headersRecord =
data.headers && data.headers.length > 0

View File

@@ -7,6 +7,7 @@ import {
NtfyIcon,
ResendIcon,
SlackIcon,
TeamsIcon,
TelegramIcon,
} from "@/components/icons/notification-icons";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -37,7 +38,7 @@ export const ShowNotifications = () => {
</CardTitle>
<CardDescription>
Add your providers to receive notifications, like Discord, Slack,
Telegram, Email, Resend, Lark.
Telegram, Teams, Email, Resend, Lark.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2 py-8 border-t">
@@ -112,6 +113,11 @@ export const ShowNotifications = () => {
<LarkIcon className="size-7 text-muted-foreground" />
</div>
)}
{notification.notificationType === "teams" && (
<div className="flex items-center justify-center rounded-lg">
<TeamsIcon className="size-7 text-muted-foreground" />
</div>
)}
{notification.name}
</span>

View File

@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, Palette, User } from "lucide-react";
import { useTranslation } from "next-i18next";
import { useEffect, useMemo, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -73,7 +72,6 @@ export const ProfileForm = () => {
isError,
error,
} = api.user.update.useMutation();
const { t } = useTranslation("settings");
const [gravatarHash, setGravatarHash] = useState<string | null>(null);
const colorInputRef = useRef<HTMLInputElement>(null);
@@ -157,10 +155,10 @@ export const ProfileForm = () => {
<div>
<CardTitle className="text-xl flex flex-row gap-2">
<User className="size-6 text-muted-foreground self-center" />
{t("settings.profile.title")}
Account
</CardTitle>
<CardDescription>
{t("settings.profile.description")}
Change the details of your profile here.
</CardDescription>
</div>
@@ -213,12 +211,9 @@ export const ProfileForm = () => {
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.profile.email")}</FormLabel>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
placeholder={t("settings.profile.email")}
{...field}
/>
<Input placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -233,7 +228,7 @@ export const ProfileForm = () => {
<FormControl>
<Input
type="password"
placeholder={t("settings.profile.password")}
placeholder="Current Password"
{...field}
value={field.value || ""}
/>
@@ -247,13 +242,11 @@ export const ProfileForm = () => {
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("settings.profile.password")}
</FormLabel>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t("settings.profile.password")}
placeholder="Password"
{...field}
value={field.value || ""}
/>
@@ -268,9 +261,7 @@ export const ProfileForm = () => {
name="image"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("settings.profile.avatar")}
</FormLabel>
<FormLabel>Avatar</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(e) => {
@@ -454,7 +445,7 @@ export const ProfileForm = () => {
<div className="flex items-center justify-end gap-2">
<Button type="submit" isLoading={isUpdating}>
{t("settings.common.save")}
Save
</Button>
</div>
</form>

View File

@@ -1,4 +1,3 @@
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { UpdateServerIp } from "@/components/dashboard/settings/web-server/update-server-ip";
import { Button } from "@/components/ui/button";
@@ -17,7 +16,6 @@ import { TerminalModal } from "../../web-server/terminal-modal";
import { GPUSupportModal } from "../gpu-support-modal";
export const ShowDokployActions = () => {
const { t } = useTranslation("settings");
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
@@ -30,13 +28,11 @@ export const ShowDokployActions = () => {
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
<Button isLoading={isLoading} variant="outline">
{t("settings.server.webServer.server.label")}
Server
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>
{t("settings.server.webServer.actions")}
</DropdownMenuLabel>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
@@ -51,17 +47,17 @@ export const ShowDokployActions = () => {
}}
className="cursor-pointer"
>
<span>{t("settings.server.webServer.reload")}</span>
<span>Reload</span>
</DropdownMenuItem>
<TerminalModal serverId="local">
<span>{t("settings.common.enterTerminal")}</span>
<span>Terminal</span>
</TerminalModal>
<ShowModalLogs appName="dokploy">
<DropdownMenuItem
className="cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
{t("settings.server.webServer.watchLogs")}
View Logs
</DropdownMenuItem>
</ShowModalLogs>
<GPUSupportModal />
@@ -70,7 +66,7 @@ export const ShowDokployActions = () => {
className="cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
{t("settings.server.webServer.updateServerIp")}
Update Server IP
</DropdownMenuItem>
</UpdateServerIp>

View File

@@ -1,4 +1,3 @@
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
@@ -16,7 +15,6 @@ interface Props {
serverId?: string;
}
export const ShowStorageActions = ({ serverId }: Props) => {
const { t } = useTranslation("settings");
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
@@ -69,13 +67,11 @@ export const ShowStorageActions = ({ serverId }: Props) => {
}
variant="outline"
>
{t("settings.server.webServer.storage.label")}
Space
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>
{t("settings.server.webServer.actions")}
</DropdownMenuLabel>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
@@ -92,9 +88,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>
{t("settings.server.webServer.storage.cleanUnusedImages")}
</span>
<span>Clean unused images</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
@@ -110,9 +104,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>
{t("settings.server.webServer.storage.cleanUnusedVolumes")}
</span>
<span>Clean unused volumes</span>
</DropdownMenuItem>
<DropdownMenuItem
@@ -129,9 +121,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>
{t("settings.server.webServer.storage.cleanStoppedContainers")}
</span>
<span>Clean stopped containers</span>
</DropdownMenuItem>
<DropdownMenuItem
@@ -165,9 +155,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>
{t("settings.server.webServer.storage.cleanDockerBuilder")}
</span>
<span>Clean Docker Builder & System</span>
</DropdownMenuItem>
{!serverId && (
<DropdownMenuItem
@@ -182,9 +170,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>
{t("settings.server.webServer.storage.cleanMonitoring")}
</span>
<span>Clean Monitoring</span>
</DropdownMenuItem>
)}
@@ -202,7 +188,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
});
}}
>
<span>{t("settings.server.webServer.storage.cleanAll")}</span>
<span>Clean all</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>

View File

@@ -1,4 +1,3 @@
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -22,7 +21,6 @@ interface Props {
serverId?: string;
}
export const ShowTraefikActions = ({ serverId }: Props) => {
const { t } = useTranslation("settings");
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
@@ -75,13 +73,11 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
}
variant="outline"
>
{t("settings.server.webServer.traefik.label")}
Traefik
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>
{t("settings.server.webServer.actions")}
</DropdownMenuLabel>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
@@ -100,7 +96,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
className="cursor-pointer"
disabled={isReloadHealthCheckExecuting}
>
<span>{t("settings.server.webServer.reload")}</span>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs
appName="dokploy-traefik"
@@ -111,7 +107,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
onSelect={(e) => e.preventDefault()}
className="cursor-pointer"
>
{t("settings.server.webServer.watchLogs")}
View Logs
</DropdownMenuItem>
</ShowModalLogs>
<EditTraefikEnv serverId={serverId}>
@@ -119,7 +115,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
onSelect={(e) => e.preventDefault()}
className="cursor-pointer"
>
<span>{t("settings.server.webServer.traefik.modifyEnv")}</span>
<span>Modify Environment</span>
</DropdownMenuItem>
</EditTraefikEnv>
@@ -176,7 +172,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
onSelect={(e) => e.preventDefault()}
className="cursor-pointer"
>
<span>{t("settings.server.webServer.traefik.managePorts")}</span>
<span>Additional Port Mappings</span>
</DropdownMenuItem>
</ManageTraefikPorts>
</DropdownMenuGroup>

View File

@@ -1,7 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -63,8 +62,6 @@ interface Props {
}
export const HandleServers = ({ serverId, asButton = false }: Props) => {
const { t } = useTranslation("settings");
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data: canCreateMoreServers, refetch } =
@@ -365,7 +362,7 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
name="ipAddress"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.terminal.ipAddress")}</FormLabel>
<FormLabel>IP Address</FormLabel>
<FormControl>
<Input placeholder="192.168.1.100" {...field} />
</FormControl>
@@ -379,7 +376,7 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.terminal.port")}</FormLabel>
<FormLabel>Port</FormLabel>
<FormControl>
<Input
placeholder="22"
@@ -409,7 +406,7 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.terminal.username")}</FormLabel>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>

View File

@@ -13,7 +13,6 @@ import {
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -52,7 +51,6 @@ import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
export const ShowServers = () => {
const { t } = useTranslation("settings");
const router = useRouter();
const query = router.query;
const { data, refetch, isLoading } = api.server.all.useQuery();

View File

@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { GlobeIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -66,7 +65,6 @@ const addServerDomain = z
type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { t } = useTranslation("settings");
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation();
@@ -119,10 +117,10 @@ export const WebDomain = () => {
<div className="flex flex-col gap-1">
<CardTitle className="text-xl flex flex-row gap-2">
<GlobeIcon className="size-6 text-muted-foreground self-center" />
{t("settings.server.domain.title")}
Server Domain
</CardTitle>
<CardDescription>
{t("settings.server.domain.description")}
Add a domain to your server application.
</CardDescription>
</div>
</CardHeader>
@@ -151,9 +149,7 @@ export const WebDomain = () => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>
{t("settings.server.domain.form.domain")}
</FormLabel>
<FormLabel>Domain</FormLabel>
<FormControl>
<Input
className="w-full"
@@ -173,9 +169,7 @@ export const WebDomain = () => {
render={({ field }) => {
return (
<FormItem>
<FormLabel>
{t("settings.server.domain.form.letsEncryptEmail")}
</FormLabel>
<FormLabel>Let's Encrypt Email</FormLabel>
<FormControl>
<Input
className="w-full"
@@ -216,32 +210,20 @@ export const WebDomain = () => {
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormLabel>
{t("settings.server.domain.form.certificate.label")}
</FormLabel>
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={t(
"settings.server.domain.form.certificate.placeholder",
)}
/>
<SelectValue placeholder="Select a certificate" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>
{t(
"settings.server.domain.form.certificateOptions.none",
)}
</SelectItem>
<SelectItem value={"none"}>None</SelectItem>
<SelectItem value={"letsencrypt"}>
{t(
"settings.server.domain.form.certificateOptions.letsencrypt",
)}
Let's Encrypt
</SelectItem>
</SelectContent>
</Select>
@@ -254,7 +236,7 @@ export const WebDomain = () => {
<div className="flex w-full justify-end col-span-2">
<Button isLoading={isLoading} type="submit">
{t("settings.common.save")}
Save
</Button>
</div>
</form>

View File

@@ -1,5 +1,4 @@
import { ServerIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import {
Card,
CardContent,
@@ -15,7 +14,6 @@ import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => {
const { t } = useTranslation("settings");
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
@@ -29,18 +27,16 @@ export const WebServer = () => {
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<ServerIcon className="size-6 text-muted-foreground self-center" />
{t("settings.server.webServer.title")}
Web Server
</CardTitle>
<CardDescription>
{t("settings.server.webServer.description")}
</CardDescription>
<CardDescription>Reload or clean the web server.</CardDescription>
</CardHeader>
{/* <CardHeader>
<CardTitle className="text-xl">
{t("settings.server.webServer.title")}
Web Server
</CardTitle>
<CardDescription>
{t("settings.server.webServer.description")}
Reload or clean the web server.
</CardDescription>
</CardHeader> */}
<CardContent className="space-y-6 py-6 border-t">

View File

@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Settings } from "lucide-react";
import { useTranslation } from "next-i18next";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
@@ -52,8 +51,6 @@ interface Props {
}
const LocalServerConfig = ({ onSave }: Props) => {
const { t } = useTranslation("settings");
const form = useForm<Schema>({
defaultValues: getLocalServerData(),
resolver: zodResolver(Schema),
@@ -77,9 +74,7 @@ const LocalServerConfig = ({ onSave }: Props) => {
<div className="flex flex-row items-center gap-2 justify-between w-full">
<div className="flex flex-row gap-2 items-center">
<Settings className="h-4 w-4" />
<span className="dark:hover:text-white">
{t("settings.terminal.connectionSettings")}
</span>
<span className="dark:hover:text-white">Connection settings</span>
</div>
</div>
</AccordionTrigger>
@@ -96,7 +91,7 @@ const LocalServerConfig = ({ onSave }: Props) => {
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.terminal.port")}</FormLabel>
<FormLabel>Port</FormLabel>
<FormControl>
<Input
{...field}
@@ -124,7 +119,7 @@ const LocalServerConfig = ({ onSave }: Props) => {
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.terminal.username")}</FormLabel>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>
@@ -142,7 +137,7 @@ const LocalServerConfig = ({ onSave }: Props) => {
className="ml-auto"
disabled={!form.formState.isDirty}
>
{t("settings.common.save")}
Save
</Button>
</AccordionContent>
</AccordionItem>

View File

@@ -1,6 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
import { useTranslation } from "next-i18next";
import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation";
import type React from "react";
import { useEffect, useState } from "react";
@@ -56,7 +55,6 @@ const TraefikPortsSchema = z.object({
type TraefikPortsForm = z.infer<typeof TraefikPortsSchema>;
export const ManageTraefikPorts = ({ children, serverId }: Props) => {
const { t } = useTranslation("settings");
const [open, setOpen] = useState(false);
const form = useForm<TraefikPortsForm>({
@@ -84,7 +82,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
isExecuting: isHealthCheckExecuting,
} = useHealthCheckAfterMutation({
initialDelay: 5000,
successMessage: t("settings.server.webServer.traefik.portsUpdated"),
successMessage: "Ports updated successfully",
onSuccess: () => {
refetchPorts();
setOpen(false);
@@ -129,14 +127,12 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<DialogContent className="sm:max-w-3xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-xl">
{t("settings.server.webServer.traefik.managePorts")}
Additional Port Mappings
</DialogTitle>
<DialogDescription className="text-base w-full">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
{t(
"settings.server.webServer.traefik.managePortsDescription",
)}
Add or remove additional ports for Traefik
<span className="text-sm text-muted-foreground">
{fields.length} port mapping{fields.length !== 1 ? "s" : ""}{" "}
configured
@@ -179,9 +175,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.targetPort",
)}
Target Port
</FormLabel>
<FormControl>
<Input
@@ -210,9 +204,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishedPort",
)}
Published Port
</FormLabel>
<FormControl>
<Input

View File

@@ -135,7 +135,9 @@ export const UpdateServer = ({
<div className="flex items-center gap-1.5 rounded-full px-3 py-1 mr-2 bg-muted">
<Server className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{dokployVersion} | {releaseTag}
{dokployVersion}{" "}
{(releaseTag === "canary" || releaseTag === "feature") &&
`(${releaseTag})`}
</span>
</div>
)}