Skip to content

Commit 97f1105

Browse files
authored
Merge pull request #3594 from Dokploy/copilot/add-network-configuration-tab
Add missing Network configuration to Swarm Settings
2 parents fa0c2ec + c650263 commit 97f1105

4 files changed

Lines changed: 325 additions & 1 deletion

File tree

apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
HealthCheckForm,
2323
LabelsForm,
2424
ModeForm,
25+
NetworkForm,
2526
PlacementForm,
2627
RestartPolicyForm,
2728
RollbackConfigForm,
@@ -79,6 +80,13 @@ const menuItems: MenuItem[] = [
7980
docDescription:
8081
"Set service mode to either 'Replicated' with a specified number of tasks (Replicas), or 'Global' (one task per node).",
8182
},
83+
{
84+
id: "network",
85+
label: "Network",
86+
description: "Configure network attachments",
87+
docDescription:
88+
"Attach the service to one or more networks. Specify the network name (Target) and optional network aliases for service discovery.",
89+
},
8290
{
8391
id: "labels",
8492
label: "Labels",
@@ -190,6 +198,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
190198
<RollbackConfigForm id={id} type={type} />
191199
)}
192200
{activeMenu === "mode" && <ModeForm id={id} type={type} />}
201+
{activeMenu === "network" && <NetworkForm id={id} type={type} />}
193202
{activeMenu === "labels" && <LabelsForm id={id} type={type} />}
194203
{activeMenu === "stop-grace-period" && (
195204
<StopGracePeriodForm id={id} type={type} />

apps/dokploy/components/dashboard/application/advanced/cluster/swarm-forms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { EndpointSpecForm } from "./endpoint-spec-form";
22
export { HealthCheckForm } from "./health-check-form";
33
export { LabelsForm } from "./labels-form";
44
export { ModeForm } from "./mode-form";
5+
export { NetworkForm } from "./network-form";
56
export { PlacementForm } from "./placement-form";
67
export { RestartPolicyForm } from "./restart-policy-form";
78
export { RollbackConfigForm } from "./rollback-config-form";
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import { zodResolver } from "@hookform/resolvers/zod";
2+
import { useEffect, useState } from "react";
3+
import { useFieldArray, useForm } from "react-hook-form";
4+
import { toast } from "sonner";
5+
import { z } from "zod";
6+
import { Button } from "@/components/ui/button";
7+
import {
8+
Form,
9+
FormControl,
10+
FormDescription,
11+
FormField,
12+
FormItem,
13+
FormLabel,
14+
FormMessage,
15+
} from "@/components/ui/form";
16+
import { Input } from "@/components/ui/input";
17+
import { api } from "@/utils/api";
18+
19+
const driverOptEntrySchema = z.object({
20+
key: z.string(),
21+
value: z.string(),
22+
});
23+
24+
export const networkFormSchema = z.object({
25+
networks: z
26+
.array(
27+
z.object({
28+
Target: z.string().optional(),
29+
Aliases: z.string().optional(),
30+
DriverOptsEntries: z.array(driverOptEntrySchema).optional(),
31+
}),
32+
)
33+
.optional(),
34+
});
35+
36+
interface NetworkFormProps {
37+
id: string;
38+
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
39+
}
40+
41+
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
42+
const [isLoading, setIsLoading] = useState(false);
43+
44+
const queryMap = {
45+
postgres: () =>
46+
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
47+
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
48+
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
49+
mariadb: () =>
50+
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
51+
application: () =>
52+
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
53+
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
54+
};
55+
const { data, refetch } = queryMap[type]
56+
? queryMap[type]()
57+
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
58+
59+
const mutationMap = {
60+
postgres: () => api.postgres.update.useMutation(),
61+
redis: () => api.redis.update.useMutation(),
62+
mysql: () => api.mysql.update.useMutation(),
63+
mariadb: () => api.mariadb.update.useMutation(),
64+
application: () => api.application.update.useMutation(),
65+
mongo: () => api.mongo.update.useMutation(),
66+
};
67+
68+
const { mutateAsync } = mutationMap[type]
69+
? mutationMap[type]()
70+
: api.mongo.update.useMutation();
71+
72+
const form = useForm<z.infer<typeof networkFormSchema>>({
73+
resolver: zodResolver(networkFormSchema),
74+
defaultValues: {
75+
networks: [],
76+
},
77+
});
78+
79+
const { fields, append, remove } = useFieldArray({
80+
control: form.control,
81+
name: "networks",
82+
});
83+
84+
useEffect(() => {
85+
if (data?.networkSwarm && Array.isArray(data.networkSwarm)) {
86+
const networkEntries = data.networkSwarm.map((network) => ({
87+
Target: network.Target || "",
88+
Aliases: network.Aliases?.join(", ") || "",
89+
DriverOptsEntries: network.DriverOpts
90+
? Object.entries(network.DriverOpts).map(([key, value]) => ({
91+
key,
92+
value: value ?? "",
93+
}))
94+
: [],
95+
}));
96+
form.reset({ networks: networkEntries });
97+
}
98+
}, [data, form]);
99+
100+
const onSubmit = async (formData: z.infer<typeof networkFormSchema>) => {
101+
setIsLoading(true);
102+
try {
103+
const networksArray =
104+
formData.networks
105+
?.filter((network) => network.Target)
106+
.map((network) => {
107+
const entries =
108+
(network.DriverOptsEntries ?? []).filter(
109+
(e) => e.key.trim() !== "",
110+
);
111+
const driverOpts =
112+
entries.length > 0
113+
? Object.fromEntries(
114+
entries.map((e) => [e.key.trim(), e.value]),
115+
)
116+
: undefined;
117+
return {
118+
Target: network.Target,
119+
Aliases: network.Aliases
120+
? network.Aliases.split(",").map((alias) => alias.trim())
121+
: undefined,
122+
DriverOpts: driverOpts,
123+
};
124+
}) || [];
125+
126+
// If no networks, send null to clear the database
127+
const networksToSend = networksArray.length > 0 ? networksArray : null;
128+
129+
await mutateAsync({
130+
applicationId: id || "",
131+
postgresId: id || "",
132+
redisId: id || "",
133+
mysqlId: id || "",
134+
mariadbId: id || "",
135+
mongoId: id || "",
136+
networkSwarm: networksToSend,
137+
});
138+
139+
toast.success("Network configuration updated successfully");
140+
refetch();
141+
} catch {
142+
toast.error("Error updating network configuration");
143+
} finally {
144+
setIsLoading(false);
145+
}
146+
};
147+
148+
return (
149+
<Form {...form}>
150+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
151+
<div>
152+
<FormLabel>Networks</FormLabel>
153+
<FormDescription>
154+
Configure network attachments for your service
155+
</FormDescription>
156+
<div className="space-y-2 mt-2">
157+
{fields.map((field, index) => (
158+
<div key={field.id} className="space-y-2 p-3 border rounded">
159+
<FormField
160+
control={form.control}
161+
name={`networks.${index}.Target`}
162+
render={({ field }) => (
163+
<FormItem>
164+
<FormLabel>Network Name</FormLabel>
165+
<FormControl>
166+
<Input {...field} placeholder="my-network" />
167+
</FormControl>
168+
<FormDescription>
169+
The name of the network to attach to
170+
</FormDescription>
171+
<FormMessage />
172+
</FormItem>
173+
)}
174+
/>
175+
<FormField
176+
control={form.control}
177+
name={`networks.${index}.Aliases`}
178+
render={({ field }) => (
179+
<FormItem>
180+
<FormLabel>Aliases (optional)</FormLabel>
181+
<FormControl>
182+
<Input
183+
{...field}
184+
placeholder="alias1, alias2, alias3"
185+
/>
186+
</FormControl>
187+
<FormDescription>
188+
Comma-separated list of network aliases
189+
</FormDescription>
190+
<FormMessage />
191+
</FormItem>
192+
)}
193+
/>
194+
<div className="space-y-2">
195+
<FormLabel>Driver options (optional)</FormLabel>
196+
<FormDescription>
197+
e.g. com.docker.network.driver.mtu, com.docker.network.driver.host_binding
198+
</FormDescription>
199+
{(form.watch(`networks.${index}.DriverOptsEntries`) ?? []).map(
200+
(_, optIndex) => (
201+
<div
202+
key={optIndex}
203+
className="flex gap-2 items-end flex-wrap"
204+
>
205+
<FormField
206+
control={form.control}
207+
name={`networks.${index}.DriverOptsEntries.${optIndex}.key`}
208+
render={({ field }) => (
209+
<FormItem className="flex-1 min-w-[140px]">
210+
<FormControl>
211+
<Input
212+
{...field}
213+
placeholder="com.docker.network.driver.mtu"
214+
/>
215+
</FormControl>
216+
<FormMessage />
217+
</FormItem>
218+
)}
219+
/>
220+
<FormField
221+
control={form.control}
222+
name={`networks.${index}.DriverOptsEntries.${optIndex}.value`}
223+
render={({ field }) => (
224+
<FormItem className="flex-1 min-w-[100px]">
225+
<FormControl>
226+
<Input {...field} placeholder="1500" />
227+
</FormControl>
228+
<FormMessage />
229+
</FormItem>
230+
)}
231+
/>
232+
<Button
233+
type="button"
234+
variant="ghost"
235+
size="sm"
236+
onClick={() => {
237+
const entries =
238+
form.getValues(
239+
`networks.${index}.DriverOptsEntries`,
240+
) ?? [];
241+
form.setValue(
242+
`networks.${index}.DriverOptsEntries`,
243+
entries.filter((_, i) => i !== optIndex),
244+
);
245+
}}
246+
>
247+
Remove
248+
</Button>
249+
</div>
250+
),
251+
)}
252+
<Button
253+
type="button"
254+
variant="outline"
255+
size="sm"
256+
onClick={() => {
257+
const entries =
258+
form.getValues(
259+
`networks.${index}.DriverOptsEntries`,
260+
) ?? [];
261+
form.setValue(
262+
`networks.${index}.DriverOptsEntries`,
263+
[...entries, { key: "", value: "" }],
264+
);
265+
}}
266+
>
267+
Add driver option
268+
</Button>
269+
</div>
270+
<Button
271+
type="button"
272+
variant="destructive"
273+
size="sm"
274+
onClick={() => remove(index)}
275+
>
276+
Remove Network
277+
</Button>
278+
</div>
279+
))}
280+
<Button
281+
type="button"
282+
variant="outline"
283+
size="sm"
284+
onClick={() =>
285+
append({
286+
Target: "",
287+
Aliases: "",
288+
DriverOptsEntries: [],
289+
})
290+
}
291+
>
292+
Add Network
293+
</Button>
294+
</div>
295+
</div>
296+
297+
<div className="flex justify-end gap-2">
298+
<Button
299+
type="button"
300+
variant="outline"
301+
onClick={() => {
302+
form.reset({ networks: [] });
303+
}}
304+
>
305+
Clear
306+
</Button>
307+
<Button type="submit" isLoading={isLoading}>
308+
Save Networks
309+
</Button>
310+
</div>
311+
</form>
312+
</Form>
313+
);
314+
};

packages/server/src/db/schema/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export const NetworkSwarmSchema = z.array(
167167
.object({
168168
Target: z.string().optional(),
169169
Aliases: z.array(z.string()).optional(),
170-
DriverOpts: z.object({}).optional(),
170+
DriverOpts: z.record(z.string()).optional(),
171171
})
172172
.strict(),
173173
);

0 commit comments

Comments
 (0)