import { zodResolver } from "@hookform/resolvers/zod"; import { Check, ChevronsUpDown, Loader2, RefreshCw } from "lucide-react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Command, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; import { api } from "@/utils/api"; interface Props { composeId: string; } const serviceSchema = z.object({ serviceName: z.string(), networkIds: z.array(z.string()), detachDokployNetwork: z.boolean(), }); const formSchema = z.object({ services: z.array(serviceSchema), }); type FormValues = z.infer; export const AssignComposeNetworks = ({ composeId }: Props) => { const [cacheType, setCacheType] = useState<"cache" | "fetch">("cache"); const { data: compose } = api.compose.one.useQuery({ composeId }); const { data: services, isLoading: isLoadingServices, error: servicesError, refetch: refetchServices, isRefetching: isRefetchingServices, } = api.compose.loadServices.useQuery( { composeId, type: cacheType }, { retry: false }, ); const onRetry = () => { setCacheType("fetch"); setTimeout(() => refetchServices(), 0); }; const { data: networks } = api.network.all.useQuery( { serverId: compose?.serverId ?? undefined }, { enabled: compose !== undefined }, ); const { mutateAsync, isPending } = api.compose.update.useMutation(); const utils = api.useUtils(); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { services: [] }, }); const { fields } = useFieldArray({ control: form.control, name: "services", }); useEffect(() => { if (!services) return; const serviceNetworks = compose?.serviceNetworks ?? []; form.reset({ services: services.map((serviceName) => { const config = serviceNetworks.find( (s) => s.serviceName === serviceName, ); return { serviceName, networkIds: config?.networkIds ?? [], detachDokployNetwork: config?.detachDokployNetwork ?? false, }; }), }); }, [services, compose?.serviceNetworks, form]); const allowsBridge = compose?.composeType === "docker-compose"; const availableNetworks = (networks ?? []).filter( (n) => allowsBridge || n.driver === "overlay", ); const onSubmit = async (values: FormValues) => { try { const serviceNetworks = values.services.filter( (s) => s.networkIds.length > 0 || s.detachDokployNetwork, ); await mutateAsync({ composeId, serviceNetworks, }); toast.success("Networks updated. Redeploy the compose to apply them."); await utils.compose.one.invalidate({ composeId }); } catch (error) { toast.error("Error updating networks", { description: error instanceof Error ? error.message : "Unknown error", }); } }; return (
Networks Attach Docker networks per service and detach it from dokploy-network. Takes effect on the next deploy.
{servicesError ? (
Could not load the compose services. If this compose was just created from a template, it hasn't been cloned yet — click Reload to clone the repository and read its services.
) : isLoadingServices ? (
Loading services...
) : !services?.length ? ( No services found in this compose. ) : (
{fields.map((fieldItem, index) => ( ))}
)}
); }; type NetworkOption = { networkId: string; name: string; driver: string }; const ServiceRow = ({ control, index, service, availableNetworks, }: { control: ReturnType>["control"]; index: number; service: string; availableNetworks: NetworkOption[]; }) => { const [open, setOpen] = useState(false); return (
( {service}
Detach dokploy-network
)} /> { const selectedNetworks = availableNetworks.filter((n) => field.value.includes(n.networkId), ); const toggle = (networkId: string) => { field.onChange( field.value.includes(networkId) ? field.value.filter((id) => id !== networkId) : [...field.value, networkId], ); }; return ( {availableNetworks.length === 0 ? (
No networks available on this server.
) : ( {availableNetworks.map((n) => { const isSelected = field.value.includes( n.networkId, ); return ( toggle(n.networkId)} className="cursor-pointer" > toggle(n.networkId)} /> {n.name} {n.driver} ); })} )}
{selectedNetworks.length > 0 && (
{selectedNetworks.map((n) => ( {n.name} ))}
)}
); }} />
); };