diff --git a/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx index 645eda903..838ee8493 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx @@ -80,8 +80,10 @@ export const EditTraefikEnv = ({ children, serverId }: Props) => { {children} - Update Traefik Env - Update the traefik env + Update Traefik Environment + + Update the traefik environment variables + {isError && {error?.message}} diff --git a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx index 180b2fcbb..86500b736 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -1,12 +1,15 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, + DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -14,59 +17,56 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; import { api } from "@/utils/api"; +import { ArrowRightLeft, Plus, Trash2 } from "lucide-react"; import { useTranslation } from "next-i18next"; import type React from "react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { z } from "zod"; +import { useFieldArray, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; -/** - * Props for the ManageTraefikPorts component - * @interface Props - * @property {React.ReactNode} children - The trigger element that opens the ports management modal - * @property {string} [serverId] - Optional ID of the server whose ports are being managed - */ interface Props { children: React.ReactNode; serverId?: string; } -/** - * Represents a port mapping configuration for Traefik - * @interface AdditionalPort - * @property {number} targetPort - The internal port that the service is listening on - * @property {number} publishedPort - The external port that will be exposed - * @property {"ingress" | "host"} publishMode - The Docker Swarm publish mode: - * - "host": Publishes the port directly on the host - * - "ingress": Publishes the port through the Swarm routing mesh - */ -interface AdditionalPort { - targetPort: number; - publishedPort: number; - publishMode: "ingress" | "host"; -} +const PortSchema = z.object({ + targetPort: z.number().min(1, "Target port is required"), + publishedPort: z.number().min(1, "Published port is required"), + publishMode: z.enum(["ingress", "host"]), +}); + +const TraefikPortsSchema = z.object({ + ports: z.array(PortSchema), +}); + +type TraefikPortsForm = z.infer; -/** - * ManageTraefikPorts is a component that provides a modal interface for managing - * additional port mappings for Traefik in a Docker Swarm environment. - * - * Features: - * - Add, remove, and edit port mappings - * - Configure target port, published port, and publish mode for each mapping - * - Persist port configurations through API calls - * - * @component - * @example - * ```tsx - * - * - * - * ``` - */ export const ManageTraefikPorts = ({ children, serverId }: Props) => { const { t } = useTranslation("settings"); const [open, setOpen] = useState(false); - const [additionalPorts, setAdditionalPorts] = useState([]); + + const form = useForm({ + resolver: zodResolver(TraefikPortsSchema), + defaultValues: { + ports: [], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "ports", + }); const { data: currentPorts, refetch: refetchPorts } = api.settings.getTraefikPorts.useQuery({ @@ -82,22 +82,19 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { useEffect(() => { if (currentPorts) { - setAdditionalPorts(currentPorts); + form.reset({ ports: currentPorts }); } - }, [currentPorts]); + }, [currentPorts, form]); const handleAddPort = () => { - setAdditionalPorts([ - ...additionalPorts, - { targetPort: 0, publishedPort: 0, publishMode: "host" }, - ]); + append({ targetPort: 0, publishedPort: 0, publishMode: "host" }); }; - const handleUpdatePorts = async () => { + const onSubmit = async (data: TraefikPortsForm) => { try { await updatePorts({ serverId, - additionalPorts, + additionalPorts: data.ports, }); toast.success(t("settings.server.webServer.traefik.portsUpdated")); setOpen(false); @@ -110,121 +107,197 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { <>
setOpen(true)}>{children}
- + - + {t("settings.server.webServer.traefik.managePorts")} - - {t("settings.server.webServer.traefik.managePortsDescription")} + +
+ {t("settings.server.webServer.traefik.managePortsDescription")} + +
-
- {additionalPorts.map((port, index) => ( -
-
- - { - const newPorts = [...additionalPorts]; - - if (newPorts[index]) { - newPorts[index].targetPort = Number.parseInt( - e.target.value, - ); - } - - setAdditionalPorts(newPorts); - }} - className="w-full rounded border p-2" - /> -
-
- - { - const newPorts = [...additionalPorts]; - if (newPorts[index]) { - newPorts[index].publishedPort = Number.parseInt( - e.target.value, - ); - } - setAdditionalPorts(newPorts); - }} - className="w-full rounded border p-2" - /> -
-
- - -
-
- -
+ +
+ +
+ {fields.length === 0 ? ( +
+ + + No port mappings configured + +

+ Add one to get started +

+
+ ) : ( +
+ {fields.map((field, index) => ( + + + ( + + + {t( + "settings.server.webServer.traefik.targetPort", + )} + + + + field.onChange(Number(e.target.value)) + } + className="w-full dark:bg-black" + placeholder="e.g. 8080" + /> + + + + )} + /> + + ( + + + {t( + "settings.server.webServer.traefik.publishedPort", + )} + + + + field.onChange(Number(e.target.value)) + } + className="w-full dark:bg-black" + placeholder="e.g. 80" + /> + + + + )} + /> + + ( + + + {t( + "settings.server.webServer.traefik.publishMode", + )} + + + + + )} + /> + +
+ +
+
+
+ ))} +
+ )} + + {fields.length > 0 && ( + +
+ + + Each port mapping defines how external traffic reaches + your containers. + +
    +
  • + Host Mode: Directly binds the port + to the host machine. +
      +
    • + Best for single-node deployments or when you + need guaranteed port availability. +
    • +
    +
  • +
  • + Ingress Mode: Routes through Docker + Swarm's load balancer. +
      +
    • + Recommended for multi-node deployments and + better scalability. +
    • +
    +
  • +
+
+
+
+ )}
- ))} -
- - -
-
+ + + + + +
); }; + +export default ManageTraefikPorts; diff --git a/apps/dokploy/public/locales/en/settings.json b/apps/dokploy/public/locales/en/settings.json index 1ce546929..39af41783 100644 --- a/apps/dokploy/public/locales/en/settings.json +++ b/apps/dokploy/public/locales/en/settings.json @@ -17,8 +17,8 @@ "settings.server.webServer.updateServerIp": "Update Server IP", "settings.server.webServer.server.label": "Server", "settings.server.webServer.traefik.label": "Traefik", - "settings.server.webServer.traefik.modifyEnv": "Modify Env", - "settings.server.webServer.traefik.managePorts": "Additional Ports", + "settings.server.webServer.traefik.modifyEnv": "Modify Environment", + "settings.server.webServer.traefik.managePorts": "Additional Port Mappings", "settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik", "settings.server.webServer.traefik.targetPort": "Target Port", "settings.server.webServer.traefik.publishedPort": "Published Port",