mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f508163e5 | |||
| fc75b847b5 | |||
| b1abcb9e06 | |||
| d542972522 | |||
| e7c38d4c54 | |||
| a8f941b5d9 | |||
| c68fac55fd | |||
| efcad7bbf5 | |||
| a0566cdbd7 | |||
| 69598821ed |
@@ -0,0 +1,563 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Network, Pencil, Plus } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const networkDriverEnum = [
|
||||
"bridge",
|
||||
"host",
|
||||
"overlay",
|
||||
"macvlan",
|
||||
"none",
|
||||
"ipvlan",
|
||||
] as const;
|
||||
|
||||
/** Sentinel for "no scope" */
|
||||
const SCOPE_EMPTY = "__scope_none__";
|
||||
|
||||
const ipamConfigEntrySchema = z.object({
|
||||
subnet: z.string().optional(),
|
||||
ipRange: z.string().optional(),
|
||||
gateway: z.string().optional(),
|
||||
});
|
||||
|
||||
const networkFormSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
driver: z.enum(networkDriverEnum).default("bridge"),
|
||||
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),
|
||||
ipamDriver: z.string().optional(),
|
||||
ipamConfig: z.array(ipamConfigEntrySchema).default([]),
|
||||
});
|
||||
|
||||
type NetworkFormValues = z.infer<typeof networkFormSchema>;
|
||||
|
||||
const defaultValues: NetworkFormValues = {
|
||||
name: "",
|
||||
driver: "bridge",
|
||||
scope: SCOPE_EMPTY,
|
||||
serverId: undefined,
|
||||
internal: false,
|
||||
attachable: false,
|
||||
ingress: false,
|
||||
configOnly: false,
|
||||
enableIPv4: true,
|
||||
enableIPv6: false,
|
||||
ipamDriver: "",
|
||||
ipamConfig: [],
|
||||
};
|
||||
|
||||
interface HandleNetworkProps {
|
||||
networkId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const HandleNetwork = ({ networkId, children }: HandleNetworkProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const utils = api.useUtils();
|
||||
const isEdit = !!networkId;
|
||||
|
||||
const { data: servers } = api.server.all.useQuery();
|
||||
const { data: network, isLoading: isLoadingNetwork } =
|
||||
api.network.one.useQuery(
|
||||
{ networkId: networkId! },
|
||||
{ enabled: isEdit && !!networkId },
|
||||
);
|
||||
|
||||
const { mutateAsync, isLoading: isPending } = networkId
|
||||
? api.network.update.useMutation()
|
||||
: api.network.create.useMutation();
|
||||
|
||||
const form = useForm<NetworkFormValues>({
|
||||
resolver: zodResolver(networkFormSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const ipamConfigFieldArray = useFieldArray({
|
||||
control: form.control,
|
||||
name: "ipamConfig",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && network && isOpen) {
|
||||
const ipam = network.ipam ?? {};
|
||||
const ipamConfigArr = (ipam.config ?? []).map((c) => ({
|
||||
subnet: c.subnet ?? "",
|
||||
ipRange: c.ipRange ?? "",
|
||||
gateway: c.gateway ?? "",
|
||||
}));
|
||||
form.reset({
|
||||
...defaultValues,
|
||||
name: network.name,
|
||||
driver: network.driver,
|
||||
scope: network.scope ?? SCOPE_EMPTY,
|
||||
serverId: network.serverId || undefined,
|
||||
internal: network.internal,
|
||||
attachable: network.attachable,
|
||||
enableIPv4: network.enableIPv4,
|
||||
enableIPv6: network.enableIPv6,
|
||||
ipamDriver: ipam.driver ?? "",
|
||||
ipamConfig: ipamConfigArr,
|
||||
ingress: network.ingress,
|
||||
configOnly: network.configOnly,
|
||||
});
|
||||
}
|
||||
}, [isEdit, isOpen, network, form]);
|
||||
|
||||
const onSubmit = async (data: NetworkFormValues) => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
await utils.network.all.invalidate();
|
||||
if (networkId) await utils.network.one.invalidate({ networkId });
|
||||
setIsOpen(false);
|
||||
form.reset(defaultValues);
|
||||
} catch {
|
||||
toast.error(isEdit ? "Error updating network" : "Error creating network");
|
||||
}
|
||||
};
|
||||
|
||||
const trigger =
|
||||
children ??
|
||||
(isEdit ? (
|
||||
<Button size="sm" variant="outline">
|
||||
<Pencil className=" size-4" />
|
||||
Edit
|
||||
</Button>
|
||||
) : (
|
||||
<Button>
|
||||
<Plus className=" size-4" />
|
||||
Add network
|
||||
</Button>
|
||||
));
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Network className="size-5 text-muted-foreground" />
|
||||
{isEdit ? "Edit network" : "Add network"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit
|
||||
? "Update this Docker network. Changes apply to name, driver, and server assignment."
|
||||
: "Create a new Docker network for your organization. You can optionally assign it to a server."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isEdit && isLoadingNetwork ? (
|
||||
<div className="flex items-center justify-center py-8 text-sm text-muted-foreground">
|
||||
Loading network…
|
||||
</div>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex w-full flex-col gap-6"
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-network" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="driver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Driver</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select driver" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{networkDriverEnum.map((d) => (
|
||||
<SelectItem key={d} value={d}>
|
||||
{d}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Server</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? undefined}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select server" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{!isCloud && (
|
||||
<SelectItem value={undefined}>
|
||||
Dokploy server
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
{isCloud
|
||||
? "Server where this network will be created."
|
||||
: "Dokploy server is the default local server; or choose a specific server."}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="scope"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Scope (optional)</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? SCOPE_EMPTY}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select scope" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value={SCOPE_EMPTY}>None</SelectItem>
|
||||
<SelectItem value="local">local</SelectItem>
|
||||
<SelectItem value="swarm">swarm</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="internal"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Internal</FormLabel>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
Restrict external access; containers on this network
|
||||
cannot reach external networks.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="attachable"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Attachable</FormLabel>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
Allow standalone containers to attach to this network
|
||||
(e.g. in Swarm, not only services).
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableIPv4"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Enable IPv4</FormLabel>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
Enable IPv4 addressing on the network.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enableIPv6"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Enable IPv6</FormLabel>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
Enable IPv6 addressing on the network.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ingress"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Ingress</FormLabel>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
Use as the routing-mesh network in Swarm mode (load
|
||||
balancing between nodes).
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="configOnly"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Config only</FormLabel>
|
||||
<FormDescription className="text-muted-foreground">
|
||||
Create a placeholder network whose config is reused by
|
||||
other networks; cannot run containers on it.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 rounded-lg border p-4">
|
||||
<FormLabel>IPAM</FormLabel>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ipamDriver"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-muted-foreground">
|
||||
Driver (optional)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="default" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<FormLabel className="text-muted-foreground">
|
||||
Config (subnet / gateway / IP range)
|
||||
</FormLabel>
|
||||
{ipamConfigFieldArray.fields.map((field, index) => (
|
||||
<div key={field.id} className="flex flex-wrap gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ipamConfig.${index}.subnet`}
|
||||
render={({ field: f }) => (
|
||||
<FormItem className="min-w-[140px] flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...f}
|
||||
placeholder="Subnet (e.g. 172.20.0.0/16)"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ipamConfig.${index}.ipRange`}
|
||||
render={({ field: f }) => (
|
||||
<FormItem className="min-w-[120px] flex-1">
|
||||
<FormControl>
|
||||
<Input {...f} placeholder="IP range" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ipamConfig.${index}.gateway`}
|
||||
render={({ field: f }) => (
|
||||
<FormItem className="min-w-[120px] flex-1">
|
||||
<FormControl>
|
||||
<Input {...f} placeholder="Gateway" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => ipamConfigFieldArray.remove(index)}
|
||||
>
|
||||
−
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
ipamConfigFieldArray.append({
|
||||
subnet: "",
|
||||
ipRange: "",
|
||||
gateway: "",
|
||||
})
|
||||
}
|
||||
>
|
||||
Add IPAM config
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending
|
||||
? isEdit
|
||||
? "Updating…"
|
||||
: "Creating…"
|
||||
: isEdit
|
||||
? "Update network"
|
||||
: "Create network"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, Network } from "lucide-react";
|
||||
import { HandleNetwork } from "@/components/dashboard/networks/handle-network";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export const ShowNetworks = () => {
|
||||
const { data: networks, isLoading } = api.network.all.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<Network className="size-6 text-muted-foreground self-center" />
|
||||
Networks
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage Docker networks for your organization. Networks can be
|
||||
scoped to a server (optional).
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{networks && networks?.length > 0 && <HandleNetwork />}
|
||||
</div>
|
||||
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
<div className="gap-4 pb-20 w-full">
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="rounded-md border">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground h-[55vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : !networks?.length ? (
|
||||
<div className="flex min-h-[55vh] w-full flex-col items-center justify-center gap-4 rounded-lg border border-dashed p-8">
|
||||
<div className="rounded-full bg-muted p-4">
|
||||
<Network className="size-10 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="space-y-1 text-center">
|
||||
<p className="text-sm font-medium">No networks yet</p>
|
||||
<p className="max-w-sm text-sm text-muted-foreground">
|
||||
Create Docker networks for your organization and
|
||||
optionally attach them to a server. Add your first
|
||||
network to get started.
|
||||
</p>
|
||||
</div>
|
||||
<HandleNetwork />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Driver</TableHead>
|
||||
<TableHead>Scope</TableHead>
|
||||
<TableHead>Internal</TableHead>
|
||||
<TableHead>Attachable</TableHead>
|
||||
<TableHead>Server</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="w-[80px]">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{networks.map((n) => (
|
||||
<TableRow key={n.networkId}>
|
||||
<TableCell className="font-medium">
|
||||
{n.name}
|
||||
</TableCell>
|
||||
<TableCell>{n.driver}</TableCell>
|
||||
<TableCell>{n.scope ?? "—"}</TableCell>
|
||||
<TableCell>{n.internal ? "Yes" : "No"}</TableCell>
|
||||
<TableCell>{n.attachable ? "Yes" : "No"}</TableCell>
|
||||
<TableCell>
|
||||
{n.serverId ?? "Dokploy server"}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(n.createdAt).toLocaleDateString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<HandleNetwork networkId={n.networkId}>
|
||||
<Button variant="ghost" size="sm">
|
||||
Edit
|
||||
</Button>
|
||||
</HandleNetwork>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
Loader2,
|
||||
LogIn,
|
||||
type LucideIcon,
|
||||
Network,
|
||||
Package,
|
||||
Palette,
|
||||
PieChart,
|
||||
@@ -206,6 +207,20 @@ const MENU: Menu = {
|
||||
isEnabled: ({ permissions, isCloud }) =>
|
||||
!!(permissions?.docker.read && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Networks",
|
||||
url: "/dashboard/networks",
|
||||
icon: Network,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Requests",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TYPE "public"."networkDriver" AS ENUM('bridge', 'host', 'overlay', 'macvlan', 'none', 'ipvlan');--> statement-breakpoint
|
||||
CREATE TABLE "network" (
|
||||
"networkId" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"driver" "networkDriver" DEFAULT 'bridge' NOT NULL,
|
||||
"scope" text,
|
||||
"internal" boolean DEFAULT false NOT NULL,
|
||||
"attachable" boolean DEFAULT false NOT NULL,
|
||||
"ingress" boolean DEFAULT false NOT NULL,
|
||||
"configOnly" boolean DEFAULT false NOT NULL,
|
||||
"enableIPv4" boolean DEFAULT true NOT NULL,
|
||||
"enableIPv6" boolean DEFAULT false NOT NULL,
|
||||
"ipam" jsonb DEFAULT '{}'::jsonb,
|
||||
"createdAt" text NOT NULL,
|
||||
"organizationId" text NOT NULL,
|
||||
"serverId" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "compose" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "mariadb" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "mongo" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "mysql" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "postgres" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "redis" ADD COLUMN "networkIds" text[] DEFAULT '{}';--> statement-breakpoint
|
||||
ALTER TABLE "network" ADD CONSTRAINT "network_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "network" ADD CONSTRAINT "network_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1163,6 +1163,13 @@
|
||||
"when": 1775845419261,
|
||||
"tag": "0165_abnormal_greymalkin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 166,
|
||||
"version": "7",
|
||||
"when": 1776117573148,
|
||||
"tag": "0166_pale_multiple_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowNetworks } from "@/components/dashboard/networks/show-networks";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Dashboard = () => {
|
||||
return <ShowNetworks />;
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
Dashboard.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { user, session } = await validateRequest(ctx.req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req } = ctx;
|
||||
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: ctx.res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
|
||||
if (user.role === "member") {
|
||||
const userR = await helpers.user.one.fetch({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (!userR?.canAccessToDocker) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await helpers.network.all.prefetch();
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { mariadbRouter } from "./routers/mariadb";
|
||||
import { mongoRouter } from "./routers/mongo";
|
||||
import { mountRouter } from "./routers/mount";
|
||||
import { mysqlRouter } from "./routers/mysql";
|
||||
import { networkRouter } from "./routers/network";
|
||||
import { notificationRouter } from "./routers/notification";
|
||||
import { organizationRouter } from "./routers/organization";
|
||||
import { patchRouter } from "./routers/patch";
|
||||
@@ -58,6 +59,7 @@ export const appRouter = createTRPCRouter({
|
||||
application: applicationRouter,
|
||||
backup: backupRouter,
|
||||
bitbucket: bitbucketRouter,
|
||||
network: networkRouter,
|
||||
certificates: certificateRouter,
|
||||
cluster: clusterRouter,
|
||||
compose: composeRouter,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
createNetwork,
|
||||
findNetworkById,
|
||||
removeNetwork,
|
||||
updateNetwork,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateNetwork,
|
||||
apiFindOneNetwork,
|
||||
apiRemoveNetwork,
|
||||
apiUpdateNetwork,
|
||||
network as networkTable,
|
||||
} from "@/server/db/schema";
|
||||
|
||||
export const networkRouter = createTRPCRouter({
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(networkTable)
|
||||
.where(eq(networkTable.organizationId, ctx.session.activeOrganizationId))
|
||||
.orderBy(desc(networkTable.createdAt));
|
||||
return rows;
|
||||
}),
|
||||
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneNetwork)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const row = await findNetworkById(input.networkId);
|
||||
if (row.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Network not found",
|
||||
});
|
||||
}
|
||||
return row;
|
||||
}),
|
||||
create: protectedProcedure
|
||||
.input(apiCreateNetwork)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return createNetwork(input, ctx.session.activeOrganizationId);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateNetwork)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const network = await findNetworkById(input.networkId);
|
||||
if (network.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Not authorized to update this network",
|
||||
});
|
||||
}
|
||||
return updateNetwork(input);
|
||||
}),
|
||||
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveNetwork)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const network = await findNetworkById(input.networkId);
|
||||
if (network.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Not authorized to delete this network",
|
||||
});
|
||||
}
|
||||
return removeNetwork(input.networkId);
|
||||
}),
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { nanoid } from "nanoid";
|
||||
import { network } from "./network";
|
||||
import { projects } from "./project";
|
||||
import { server } from "./server";
|
||||
import { ssoProvider } from "./sso";
|
||||
@@ -108,6 +109,7 @@ export const organizationRelations = relations(
|
||||
references: [user.id],
|
||||
}),
|
||||
servers: many(server),
|
||||
networks: many(network),
|
||||
projects: many(projects),
|
||||
members: many(member),
|
||||
ssoProviders: many(ssoProvider),
|
||||
|
||||
@@ -227,6 +227,7 @@ export const applications = pgTable("application", {
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
networkIds: text("networkIds").array().default([]),
|
||||
});
|
||||
|
||||
export const applicationsRelations = relations(
|
||||
@@ -368,6 +369,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
||||
watchPaths: z.array(z.string()).optional().optional(),
|
||||
previewLabels: z.array(z.string()).optional(),
|
||||
networkIds: z.array(z.string()).optional(),
|
||||
cleanCache: z.boolean().optional(),
|
||||
stopGracePeriodSwarm: z.number().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
|
||||
@@ -108,6 +108,7 @@ export const compose = pgTable("compose", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
networkIds: text("networkIds").array().default([]),
|
||||
});
|
||||
|
||||
export const composeRelations = relations(compose, ({ one, many }) => ({
|
||||
|
||||
@@ -18,6 +18,7 @@ export * from "./libsql";
|
||||
export * from "./mariadb";
|
||||
export * from "./mongo";
|
||||
export * from "./mount";
|
||||
export * from "./network";
|
||||
export * from "./mysql";
|
||||
export * from "./notification";
|
||||
export * from "./patch";
|
||||
|
||||
@@ -87,6 +87,7 @@ export const mariadb = pgTable("mariadb", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
networkIds: text("networkIds").array().default([]),
|
||||
});
|
||||
|
||||
export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
|
||||
|
||||
@@ -91,6 +91,7 @@ export const mongo = pgTable("mongo", {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
replicaSets: boolean("replicaSets").default(false),
|
||||
networkIds: text("networkIds").array().default([]),
|
||||
});
|
||||
|
||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||
|
||||
@@ -85,6 +85,7 @@ export const mysql = pgTable("mysql", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
networkIds: text("networkIds").array().default([]),
|
||||
});
|
||||
|
||||
export const mysqlRelations = relations(mysql, ({ one, many }) => ({
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, jsonb, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { organization } from "./account";
|
||||
import { server } from "./server";
|
||||
|
||||
/** Docker network driver types */
|
||||
export const networkDriver = pgEnum("networkDriver", [
|
||||
"bridge",
|
||||
"host",
|
||||
"overlay",
|
||||
"macvlan",
|
||||
"none",
|
||||
"ipvlan",
|
||||
]);
|
||||
|
||||
export const network = pgTable("network", {
|
||||
networkId: text("networkId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
driver: networkDriver("driver").notNull().default("bridge"),
|
||||
scope: text("scope"), // e.g. "local", "swarm"
|
||||
internal: boolean("internal").notNull().default(false),
|
||||
attachable: boolean("attachable").notNull().default(false),
|
||||
ingress: boolean("ingress").notNull().default(false),
|
||||
configOnly: boolean("configOnly").notNull().default(false),
|
||||
enableIPv4: boolean("enableIPv4").notNull().default(true),
|
||||
enableIPv6: boolean("enableIPv6").notNull().default(false),
|
||||
ipam: jsonb("ipam")
|
||||
.$type<{
|
||||
driver?: string;
|
||||
config?: Array<{ subnet?: string; gateway?: string; ipRange?: string }>;
|
||||
}>()
|
||||
.default({}),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const networkRelations = relations(network, ({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [network.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
server: one(server, {
|
||||
fields: [network.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(network, {
|
||||
networkId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
driver: z
|
||||
.enum(["bridge", "host", "overlay", "macvlan", "none", "ipvlan"])
|
||||
.optional(),
|
||||
scope: z.string().optional(),
|
||||
internal: z.boolean().optional(),
|
||||
attachable: z.boolean().optional(),
|
||||
ingress: z.boolean().optional(),
|
||||
configOnly: z.boolean().optional(),
|
||||
enableIPv4: z.boolean().optional(),
|
||||
enableIPv6: z.boolean().optional(),
|
||||
ipam: z
|
||||
.object({
|
||||
driver: z.string().optional(),
|
||||
config: z
|
||||
.array(
|
||||
z.object({
|
||||
subnet: z.string().optional(),
|
||||
gateway: z.string().optional(),
|
||||
ipRange: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
organizationId: z.string().min(1),
|
||||
serverId: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateNetwork = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
driver: true,
|
||||
scope: true,
|
||||
internal: true,
|
||||
attachable: true,
|
||||
ingress: true,
|
||||
configOnly: true,
|
||||
enableIPv4: true,
|
||||
enableIPv6: true,
|
||||
ipam: true,
|
||||
serverId: true,
|
||||
})
|
||||
.partial()
|
||||
.required({ name: true });
|
||||
|
||||
export const apiFindOneNetwork = createSchema
|
||||
.pick({
|
||||
networkId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiRemoveNetwork = createSchema
|
||||
.pick({
|
||||
networkId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateNetwork = createSchema
|
||||
.pick({
|
||||
networkId: true,
|
||||
name: true,
|
||||
driver: true,
|
||||
scope: true,
|
||||
internal: true,
|
||||
attachable: true,
|
||||
ingress: true,
|
||||
configOnly: true,
|
||||
enableIPv4: true,
|
||||
enableIPv6: true,
|
||||
ipam: true,
|
||||
serverId: true,
|
||||
})
|
||||
.partial()
|
||||
.required({ networkId: true });
|
||||
@@ -85,6 +85,7 @@ export const postgres = pgTable("postgres", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
networkIds: text("networkIds").array().default([]),
|
||||
});
|
||||
|
||||
export const postgresRelations = relations(postgres, ({ one, many }) => ({
|
||||
|
||||
@@ -75,6 +75,7 @@ export const redis = pgTable("redis", {
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
networkIds: text("networkIds").array().default([]),
|
||||
});
|
||||
|
||||
export const redisRelations = relations(redis, ({ one, many }) => ({
|
||||
|
||||
@@ -19,6 +19,7 @@ import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
import { network } from "./network";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
import { schedules } from "./schedule";
|
||||
@@ -124,6 +125,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
|
||||
mysql: many(mysql),
|
||||
postgres: many(postgres),
|
||||
certificates: many(certificates),
|
||||
networks: many(network),
|
||||
organization: one(organization, {
|
||||
fields: [server.organizationId],
|
||||
references: [organization.id],
|
||||
|
||||
@@ -28,6 +28,7 @@ export * from "./services/mariadb";
|
||||
export * from "./services/mongo";
|
||||
export * from "./services/mount";
|
||||
export * from "./services/mysql";
|
||||
export * from "./services/network";
|
||||
export * from "./services/notification";
|
||||
export * from "./services/patch";
|
||||
export * from "./services/patch-repo";
|
||||
|
||||
@@ -100,6 +100,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
|
||||
domains: true,
|
||||
deployments: true,
|
||||
mounts: true,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateNetwork,
|
||||
type apiUpdateNetwork,
|
||||
network,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
|
||||
export const findNetworkById = async (networkId: string) => {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(network)
|
||||
.where(eq(network.networkId, networkId))
|
||||
.limit(1);
|
||||
|
||||
if (!row) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Network not found",
|
||||
});
|
||||
}
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
export const createNetwork = async (
|
||||
input: typeof apiCreateNetwork._type,
|
||||
organizationId: string,
|
||||
) => {
|
||||
if (IS_CLOUD) {
|
||||
if (!input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Server is required",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const created = await db.transaction(async (tx) => {
|
||||
const [row] = await tx
|
||||
.insert(network)
|
||||
.values({
|
||||
...input,
|
||||
organizationId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!row) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to create network",
|
||||
});
|
||||
}
|
||||
|
||||
const ipam = row.ipam ?? {};
|
||||
const ipamConfig = (ipam.config ?? [])
|
||||
.map((c) => {
|
||||
const entry: Record<string, string> = {};
|
||||
if (c.subnet) entry.Subnet = c.subnet;
|
||||
if (c.gateway) entry.Gateway = c.gateway;
|
||||
if (c.ipRange) entry.IPRange = c.ipRange;
|
||||
return entry;
|
||||
})
|
||||
.filter((e) => Object.keys(e).length > 0);
|
||||
|
||||
const docker = await getRemoteDocker(input.serverId ?? null);
|
||||
await docker.createNetwork({
|
||||
Name: row.name,
|
||||
Driver: row.driver,
|
||||
Internal: row.internal,
|
||||
Attachable: row.attachable,
|
||||
Ingress: row.ingress,
|
||||
EnableIPv6: row.enableIPv6,
|
||||
IPAM: {
|
||||
Driver: ipam.driver ?? "default",
|
||||
Config: ipamConfig.length > 0 ? ipamConfig : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
return created;
|
||||
};
|
||||
|
||||
export const updateNetwork = async (input: typeof apiUpdateNetwork._type) => {
|
||||
const { networkId, ...rest } = input;
|
||||
const [updated] = await db
|
||||
.update(network)
|
||||
.set(rest)
|
||||
.where(eq(network.networkId, networkId))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Network not found",
|
||||
});
|
||||
}
|
||||
|
||||
return updated;
|
||||
};
|
||||
|
||||
export const removeNetwork = async (networkId: string) => {
|
||||
const [deleted] = await db
|
||||
.delete(network)
|
||||
.where(eq(network.networkId, networkId))
|
||||
.returning();
|
||||
|
||||
if (!deleted) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Network not found",
|
||||
});
|
||||
}
|
||||
|
||||
return deleted;
|
||||
};
|
||||
Reference in New Issue
Block a user