From 9b18faa717e95cccf43fa04130335c8d3fe4c1c6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 14 Nov 2025 01:47:23 -0600 Subject: [PATCH] feat: enhance swarm settings configuration form - Replaced JSON editors with form fields for better user experience. - Added support for dynamic input fields for health check commands and placement constraints. - Improved validation and error handling for swarm settings. - Updated descriptions and tooltips for clarity on configuration options. --- .../cluster/modify-swarm-settings.tsx | 1599 ++++++++++++----- 1 file changed, 1113 insertions(+), 486 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx index 739bd87a5..9d43a3de9 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/modify-swarm-settings.tsx @@ -1,11 +1,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { HelpCircle, Settings } from "lucide-react"; +import { HelpCircle, Plus, Settings, Trash2 } from "lucide-react"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; -import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -26,6 +25,14 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, @@ -138,65 +145,17 @@ const EndpointSpecSwarmSchema = z }) .strict(); -const createStringToJSONSchema = (schema: z.ZodTypeAny) => { - return z - .string() - .transform((str, ctx) => { - if (str === null || str === "") { - return null; - } - try { - return JSON.parse(str); - } catch { - ctx.addIssue({ code: "custom", message: "Invalid JSON format" }); - return z.NEVER; - } - }) - .superRefine((data, ctx) => { - if (data === null) { - return; - } - - if (Object.keys(data).length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Object cannot be empty", - }); - return; - } - - const parseResult = schema.safeParse(data); - if (!parseResult.success) { - for (const error of parseResult.error.issues) { - const path = error.path.join("."); - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `${path} ${error.message}`, - }); - } - } - }); -}; - const addSwarmSettings = z.object({ - healthCheckSwarm: createStringToJSONSchema(HealthCheckSwarmSchema).nullable(), - restartPolicySwarm: createStringToJSONSchema( - RestartPolicySwarmSchema, - ).nullable(), - placementSwarm: createStringToJSONSchema(PlacementSwarmSchema).nullable(), - updateConfigSwarm: createStringToJSONSchema( - UpdateConfigSwarmSchema, - ).nullable(), - rollbackConfigSwarm: createStringToJSONSchema( - UpdateConfigSwarmSchema, - ).nullable(), - modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(), - labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(), - networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), + healthCheckSwarm: HealthCheckSwarmSchema.nullable(), + restartPolicySwarm: RestartPolicySwarmSchema.nullable(), + placementSwarm: PlacementSwarmSchema.nullable(), + updateConfigSwarm: UpdateConfigSwarmSchema.nullable(), + rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(), + modeSwarm: ServiceModeSwarmSchema.nullable(), + labelsSwarm: LabelsSwarmSchema.nullable(), + networkSwarm: NetworkSwarmSchema.nullable(), stopGracePeriodSwarm: z.bigint().nullable(), - endpointSpecSwarm: createStringToJSONSchema( - EndpointSpecSwarmSchema, - ).nullable(), + endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(), }); type AddSwarmSettings = z.infer; @@ -270,34 +229,16 @@ export const AddSwarmSettings = ({ id, type }: Props) => { ? stopGracePeriodValue : BigInt(stopGracePeriodValue); form.reset({ - healthCheckSwarm: data.healthCheckSwarm - ? JSON.stringify(data.healthCheckSwarm, null, 2) - : null, - restartPolicySwarm: data.restartPolicySwarm - ? JSON.stringify(data.restartPolicySwarm, null, 2) - : null, - placementSwarm: data.placementSwarm - ? JSON.stringify(data.placementSwarm, null, 2) - : null, - updateConfigSwarm: data.updateConfigSwarm - ? JSON.stringify(data.updateConfigSwarm, null, 2) - : null, - rollbackConfigSwarm: data.rollbackConfigSwarm - ? JSON.stringify(data.rollbackConfigSwarm, null, 2) - : null, - modeSwarm: data.modeSwarm - ? JSON.stringify(data.modeSwarm, null, 2) - : null, - labelsSwarm: data.labelsSwarm - ? JSON.stringify(data.labelsSwarm, null, 2) - : null, - networkSwarm: data.networkSwarm - ? JSON.stringify(data.networkSwarm, null, 2) - : null, + healthCheckSwarm: data.healthCheckSwarm || null, + restartPolicySwarm: data.restartPolicySwarm || null, + placementSwarm: data.placementSwarm || null, + updateConfigSwarm: data.updateConfigSwarm || null, + rollbackConfigSwarm: data.rollbackConfigSwarm || null, + modeSwarm: data.modeSwarm || null, + labelsSwarm: data.labelsSwarm || null, + networkSwarm: data.networkSwarm || null, stopGracePeriodSwarm: normalizedStopGracePeriod, - endpointSpecSwarm: data.endpointSpecSwarm - ? JSON.stringify(data.endpointSpecSwarm, null, 2) - : null, + endpointSpecSwarm: data.endpointSpecSwarm || null, }); } }, [form, form.reset, data]); @@ -341,7 +282,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => { Swarm Settings - Update certain settings using a json object. + Configure swarm settings using form fields. {isError && {error?.message}} @@ -358,204 +299,512 @@ export const AddSwarmSettings = ({ id, type }: Props) => { onSubmit={form.handleSubmit(onSubmit)} className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative mt-4" > - ( - - Health Check - - - - - Check the interface - - - - - -
-														{`{
+						
+ Health Check + + + + + Health check configuration + + + + + +
+												{`{
 	Test?: string[] | undefined;
 	Interval?: number | undefined;
 	Timeout?: number | undefined;
 	StartPeriod?: number | undefined;
 	Retries?: number | undefined;
 }`}
-													
-
-
-
-
+
+
+
+
+
- - - -
-										
-									
-
- )} - /> + { + const testArray = form.watch("healthCheckSwarm.Test") || []; + return ( + <> +
+ Test Commands +
+ {testArray.map((_, index) => ( +
+ + { + const newArray = [...testArray]; + newArray[index] = e.target.value; + form.setValue("healthCheckSwarm", { + ...form.getValues("healthCheckSwarm"), + Test: newArray, + }); + }} + /> + + +
+ ))} + +
+
- ( - - Restart Policy - - - - - Check the interface - - - - - -
-														{`{
+											
+ ( + + Interval (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Timeout (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Start Period (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Retries + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> +
+ + ); + }} + /> + } + /> + + +
+ Restart Policy + + + + + Restart policy configuration + + + + + +
+												{`{
 	Condition?: string | undefined;
 	Delay?: number | undefined;
 	MaxAttempts?: number | undefined;
 	Window?: number | undefined;
 }`}
-													
-
-
-
-
+
+
+
+
+
- - - -
-										
-									
-
- )} - /> - - ( - - Placement - - - - - Check the interface - - - - + ( + + Condition + + + + )} + /> + ( + + Delay (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Max Attempts + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Window (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + + } + /> + + +
+ Placement + + + + + Placement configuration + + + + + +
+												{`{
 	Constraints?: string[] | undefined;
 	Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
 	MaxReplicas?: number | undefined;
-	Platforms?:
-		| Array<{
-				Architecture: string;
-				OS: string;
-		  }>
-		| undefined;
+	Platforms?: Array<{ Architecture: string; OS: string }> | undefined;
 }`}
-													
-
-
-
-
+ + + + + - - - -
-										
-									
- - )} - /> + { + const constraints = + form.watch("placementSwarm.Constraints") || []; + const preferences = + form.watch("placementSwarm.Preferences") || []; + const platforms = + form.watch("placementSwarm.Platforms") || []; + return ( + <> +
+ Constraints +
+ {constraints.map((_, index) => ( +
+ + { + const newArray = [...constraints]; + newArray[index] = e.target.value; + form.setValue("placementSwarm", { + ...form.getValues("placementSwarm"), + Constraints: newArray, + }); + }} + /> + + +
+ ))} + +
+
- ( - - Update Config - - - - - Check the interface - - - - - -
-														{`{
+											
+ Max Replicas + ( + + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> +
+ + ); + }} + /> + } + /> +
+ +
+ Update Config + + + + + Update configuration + + + + + +
+												{`{
 	Parallelism?: number;
 	Delay?: number | undefined;
 	FailureAction?: string | undefined;
@@ -563,57 +812,186 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
 	MaxFailureRatio?: number | undefined;
 	Order: string;
 }`}
-													
-
-
-
-
+ +
+ + + - - - -
-										
-									
- - )} - /> - - ( - - Rollback Config - - - - - Check the interface - - - - + ( + + Parallelism * + + + field.onChange(Number(e.target.value)) + } + /> + + + + )} + /> + ( + + Order * + + + + )} + /> + ( + + Delay (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Failure Action + + + + )} + /> + ( + + Monitor (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Max Failure Ratio + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> +
+ } + /> + + +
+ Rollback Config + + + + + Rollback configuration + + + + + +
+												{`{
 	Parallelism?: number;
 	Delay?: number | undefined;
 	FailureAction?: string | undefined;
@@ -621,109 +999,303 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
 	MaxFailureRatio?: number | undefined;
 	Order: string;
 }`}
-													
-
-
-
-
+ + + + + - - - -
-										
-									
- - )} - /> - - ( - - Mode - - - - - Check the interface - - - - + ( + + Parallelism * + + + field.onChange(Number(e.target.value)) + } + /> + + + + )} + /> + ( + + Order * + + + + )} + /> + ( + + Delay (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Failure Action + + + + )} + /> + ( + + Monitor (nanoseconds) + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> + ( + + Max Failure Ratio + + + field.onChange( + e.target.value + ? Number(e.target.value) + : undefined, + ) + } + /> + + + + )} + /> +
+ } + /> + + +
+ Mode + + + + + Service mode configuration + + + + + +
+												{`{
 	Replicated?: { Replicas?: number | undefined } | undefined;
 	Global?: {} | undefined;
-	ReplicatedJob?:
-		| {
-				MaxConcurrent?: number | undefined;
-				TotalCompletions?: number | undefined;
-		  }
-		| undefined;
+	ReplicatedJob?: { MaxConcurrent?: number | undefined; TotalCompletions?: number | undefined } | undefined;
 	GlobalJob?: {} | undefined;
 }`}
-													
-
-
-
-
+ + + + + - - - -
-										
-									
- - )} - /> +
+ ( + + Replicated - Replicas + + { + const current = form.getValues("modeSwarm") || {}; + form.setValue("modeSwarm", { + ...current, + Replicated: e.target.value + ? { Replicas: Number(e.target.value) } + : undefined, + }); + }} + /> + + + + )} + /> + ( + + ReplicatedJob - Max Concurrent + + { + const current = form.getValues("modeSwarm") || {}; + form.setValue("modeSwarm", { + ...current, + ReplicatedJob: e.target.value + ? { + ...current.ReplicatedJob, + MaxConcurrent: Number(e.target.value), + } + : undefined, + }); + }} + /> + + + + )} + /> + ( + + ReplicatedJob - Total Completions + + { + const current = form.getValues("modeSwarm") || {}; + form.setValue("modeSwarm", { + ...current, + ReplicatedJob: e.target.value + ? { + ...current.ReplicatedJob, + TotalCompletions: Number(e.target.value), + } + : undefined, + }); + }} + /> + + + + )} + /> +
+ } + /> +
( - + Network - Check the interface + Network configuration (JSON array) @@ -747,28 +1319,29 @@ export const AddSwarmSettings = ({ id, type }: Props) => { - { + try { + const value = e.target.value.trim(); + if (!value) { + field.onChange(null); + return; + } + const parsed = JSON.parse(value); + field.onChange(parsed); + } catch { + // Invalid JSON, but let validation handle it + field.onChange(e.target.value as any); + } + }} /> -
-										
-									
+
)} /> @@ -776,13 +1349,13 @@ export const AddSwarmSettings = ({ id, type }: Props) => { control={form.control} name="labelsSwarm" render={({ field }) => ( - + Labels - Check the interface + Labels as key-value pairs (JSON object) @@ -802,20 +1375,29 @@ export const AddSwarmSettings = ({ id, type }: Props) => { - { + try { + const value = e.target.value.trim(); + if (!value) { + field.onChange(null); + return; + } + const parsed = JSON.parse(value); + field.onChange(parsed); + } catch { + // Invalid JSON, but let validation handle it + field.onChange(e.target.value as any); + } + }} /> -
-										
-									
+
)} /> @@ -870,28 +1452,24 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
)} /> - ( - - Endpoint Spec - - - - - Check the interface - - - - - -
-														{`{
+						
+ Endpoint Spec + + + + + Endpoint specification + + + + + +
+												{`{
 	Mode?: string | undefined;
 	Ports?: Array<{
 		Protocol?: string | undefined;
@@ -900,37 +1478,86 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
 		PublishMode?: string | undefined;
 	}> | undefined;
 }`}
-													
-
-
-
-
+
+
+
+
+
- - - -
-										
-									
-
- )} - /> +
+ ( + + Mode + + + + )} + /> +
+
+ Ports (JSON array) + ( + + +