diff --git a/apps/dokploy/components/dashboard/networks/handle-network.tsx b/apps/dokploy/components/dashboard/networks/handle-network.tsx index 86d328b8e..8e1dc5087 100644 --- a/apps/dokploy/components/dashboard/networks/handle-network.tsx +++ b/apps/dokploy/components/dashboard/networks/handle-network.tsx @@ -1,7 +1,7 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Network, Pencil, Plus } from "lucide-react"; +import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; +import { Network, Pencil, Plus, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -47,6 +47,8 @@ const networkDriverEnum = [ /** Sentinel for "no scope" */ const SCOPE_EMPTY = "__scope_none__"; +/** Sentinel for the local Dokploy server (no serverId) */ +const SERVER_LOCAL = "__server_local__"; const ipamConfigEntrySchema = z.object({ subnet: z.string().optional(), @@ -56,17 +58,17 @@ const ipamConfigEntrySchema = z.object({ const networkFormSchema = z.object({ name: z.string().min(1, "Name is required"), - driver: z.enum(networkDriverEnum).default("bridge"), + driver: z.enum(networkDriverEnum), scope: z.string().optional(), serverId: z.string().optional(), - internal: z.boolean().default(false), - attachable: z.boolean().default(false), - ingress: z.boolean().default(false), - configOnly: z.boolean().default(false), - enableIPv4: z.boolean().default(true), - enableIPv6: z.boolean().default(false), + internal: z.boolean(), + attachable: z.boolean(), + ingress: z.boolean(), + configOnly: z.boolean(), + enableIPv4: z.boolean(), + enableIPv6: z.boolean(), ipamDriver: z.string().optional(), - ipamConfig: z.array(ipamConfigEntrySchema).default([]), + ipamConfig: z.array(ipamConfigEntrySchema), }); type NetworkFormValues = z.infer; @@ -86,6 +88,39 @@ const defaultValues: NetworkFormValues = { ipamConfig: [], }; +const toggleOptions = [ + { + name: "internal", + label: "Internal", + description: "Containers on this network cannot reach external networks.", + }, + { + name: "attachable", + label: "Attachable", + description: "Allow standalone containers to attach to this network.", + }, + { + name: "enableIPv4", + label: "Enable IPv4", + description: "Enable IPv4 addressing on the network.", + }, + { + name: "enableIPv6", + label: "Enable IPv6", + description: "Enable IPv6 addressing on the network.", + }, + { + name: "ingress", + label: "Ingress", + description: "Use as the routing-mesh network in Swarm mode.", + }, + { + name: "configOnly", + label: "Config only", + description: "Placeholder network whose config is reused by others.", + }, +] as const; + interface HandleNetworkProps { networkId?: string; children?: React.ReactNode; @@ -104,9 +139,9 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { { enabled: isEdit && !!networkId }, ); - const { mutateAsync, isLoading: isPending } = networkId - ? api.network.update.useMutation() - : api.network.create.useMutation(); + const createMutation = api.network.create.useMutation(); + const updateMutation = api.network.update.useMutation(); + const isPending = isEdit ? updateMutation.isPending : createMutation.isPending; const form = useForm({ resolver: zodResolver(networkFormSchema), @@ -148,25 +183,31 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { const scope = data.scope && data.scope !== SCOPE_EMPTY ? data.scope : undefined; - try { - await mutateAsync({ - networkId: networkId ?? "", - name: data.name, - driver: data.driver, - scope, - serverId: data.serverId || undefined, - internal: data.internal, - attachable: data.attachable, - ingress: data.ingress, - configOnly: data.configOnly, - enableIPv4: data.enableIPv4, - enableIPv6: data.enableIPv6, - ipam: { - driver: data.ipamDriver, - config: data.ipamConfig, - }, - }); + const payload = { + name: data.name, + driver: data.driver, + scope, + serverId: data.serverId || undefined, + internal: data.internal, + attachable: data.attachable, + ingress: data.ingress, + configOnly: data.configOnly, + enableIPv4: data.enableIPv4, + enableIPv6: data.enableIPv6, + ipam: { + driver: data.ipamDriver, + config: data.ipamConfig, + }, + }; + try { + if (isEdit && networkId) { + await updateMutation.mutateAsync({ networkId, ...payload }); + } else { + await createMutation.mutateAsync(payload); + } + + toast.success(isEdit ? "Network updated" : "Network created"); await utils.network.all.invalidate(); if (networkId) await utils.network.one.invalidate({ networkId }); setIsOpen(false); @@ -193,7 +234,7 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { return ( {trigger} - + @@ -257,199 +298,111 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { )} /> - ( - - Server - - - {isCloud - ? "Server where this network will be created." - : "Dokploy server is the default local server; or choose a specific server."} - - - - )} - /> - ( - - Scope (optional) - - - - )} - />
( - -
- Internal - - Restrict external access; containers on this network - cannot reach external networks. - -
- - - + + Server + + + {isCloud + ? "Server where this network will be created." + : "Dokploy server is the default local server."} + + )} /> ( - -
- Attachable - - Allow standalone containers to attach to this network - (e.g. in Swarm, not only services). - -
- - - -
- )} - /> - ( - -
- Enable IPv4 - - Enable IPv4 addressing on the network. - -
- - - -
- )} - /> - ( - -
- Enable IPv6 - - Enable IPv6 addressing on the network. - -
- - - -
- )} - /> - ( - -
- Ingress - - Use as the routing-mesh network in Swarm mode (load - balancing between nodes). - -
- - - -
- )} - /> - ( - -
- Config only - - Create a placeholder network whose config is reused by - other networks; cannot run containers on it. - -
- - - + + Scope (optional) + + )} />
-
- IPAM +
+ {toggleOptions.map((option) => ( + ( + +
+ {option.label} + + {option.description} + +
+ + + +
+ )} + /> + ))} +
+
+
+ IPAM +

+ IP address management settings for this network. +

+
{ type="button" variant="outline" size="icon" + aria-label="Remove IPAM config" onClick={() => ipamConfigFieldArray.remove(index)} > - − +
))} @@ -532,6 +486,7 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { }) } > + Add IPAM config
@@ -544,14 +499,8 @@ export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => { > Cancel - diff --git a/packages/server/src/services/network.ts b/packages/server/src/services/network.ts index 2fe764ab6..7934e2ebc 100644 --- a/packages/server/src/services/network.ts +++ b/packages/server/src/services/network.ts @@ -6,6 +6,7 @@ import { } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import type { z } from "zod"; import { IS_CLOUD } from "../constants"; import { getRemoteDocker } from "../utils/servers/remote-docker"; @@ -27,7 +28,7 @@ export const findNetworkById = async (networkId: string) => { }; export const createNetwork = async ( - input: typeof apiCreateNetwork._type, + input: z.infer, organizationId: string, ) => { if (IS_CLOUD) { @@ -86,7 +87,9 @@ export const createNetwork = async ( return created; }; -export const updateNetwork = async (input: typeof apiUpdateNetwork._type) => { +export const updateNetwork = async ( + input: z.infer, +) => { const { networkId, ...rest } = input; const [updated] = await db .update(network)