From 0db98c0b92c5241ea6ace97ea247a7ca2684c3d7 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 23 Dec 2024 21:05:43 -0500 Subject: [PATCH 1/2] feat(traefik/ports): improved UI --- .../settings/web-server/edit-traefik-env.tsx | 6 +- .../web-server/manage-traefik-ports.tsx | 289 +++++++++++------- apps/dokploy/public/locales/en/settings.json | 4 +- 3 files changed, 193 insertions(+), 106 deletions(-) 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..66525a0b2 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,11 +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 { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, @@ -15,6 +19,7 @@ import { SelectValue, } from "@/components/ui/select"; 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"; @@ -67,6 +72,7 @@ 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 { data: currentPorts, refetch: refetchPorts } = api.settings.getTraefikPorts.useQuery({ @@ -91,6 +97,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { ...additionalPorts, { targetPort: 0, publishedPort: 0, publishMode: "host" }, ]); + setIsDirty(true); }; const handleUpdatePorts = async () => { @@ -101,130 +108,208 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => { }); 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}
- + - + {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" - /> -
-
- - -
-
- -
+ +
+ {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" + /> +
+ +
+ + { + 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" + /> +
+ +
+ + +
+ +
+ +
+
+
+ ))} +
+ )} + + {additionalPorts.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. +
    • +
    +
  • +
+
+
+
+ )} +
+ + + {(additionalPorts.length > 0 || isDirty) && ( -
-
+ )} +
); }; + +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", From ce19a42aee3db7e8d35c1487a977cc0e61b3da22 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 24 Dec 2024 14:47:42 -0600 Subject: [PATCH 2/2] refactor: use react hook form --- .../web-server/manage-traefik-ports.tsx | 404 +++++++++--------- 1 file changed, 196 insertions(+), 208 deletions(-) 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 66525a0b2..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 @@ -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) && ( - - )} - + + + + +