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 66525a0b..86500b73 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 @@ -10,7 +10,6 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Select, SelectContent, @@ -18,61 +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 [isDirty, setIsDirty] = useState(false); + + 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({ @@ -88,38 +82,27 @@ 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" }, - ]); - setIsDirty(true); + 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); - setIsDirty(false); } catch (error) { toast.error(t("settings.server.webServer.traefik.portsUpdateError")); } }; - const handleRemovePort = (index: number) => { - const newPorts = additionalPorts.filter((_, i) => i !== index); - setAdditionalPorts(newPorts); - setIsDirty(true); - }; - return ( <>
setOpen(true)}>{children}
@@ -144,168 +127,173 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { -
- {additionalPorts.length === 0 ? ( -
- - - No port mappings configured - -

- Add one to get started -

-
- ) : ( -
- {additionalPorts.map((port, index) => ( - - -
- - { - const newPorts = [...additionalPorts]; - if (newPorts[index]) { - newPorts[index].targetPort = Number.parseInt( - e.target.value, - ); - } - setAdditionalPorts(newPorts); - }} - className="w-full dark:bg-black" - placeholder="e.g. 8080" - /> -
+
+ +
+ {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" + /> + + + + )} + /> -
- - { - const newPorts = [...additionalPorts]; - if (newPorts[index]) { - newPorts[index].publishedPort = Number.parseInt( - e.target.value, - ); - } - setAdditionalPorts(newPorts); - }} - className="w-full dark:bg-black" - placeholder="e.g. 80" - /> -
+ ( + + + {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", + )} + + + + + )} + /> -
- -
-
-
- ))} -
- )} +
+ +
+ + + ))} +
+ )} - {additionalPorts.length > 0 && ( - -
- - - Each port mapping defines how external traffic reaches - your containers. - -
    -
  • - Host Mode: Directly binds the port to - the host machine. -
      + {fields.length > 0 && ( + +
      + + + Each port mapping defines how external traffic reaches + your containers. + +
      • - Best for single-node deployments or when you need - guaranteed port availability. + 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. +
        • +
      - -
    • - Ingress Mode: Routes through Docker - Swarm's load balancer. -
        -
      • - Recommended for multi-node deployments and better - scalability. -
      • -
      -
    • -
    - -
-
- )} -
+ +
+ + )} + - - {(additionalPorts.length > 0 || isDirty) && ( - - )} - + + + + +