Skip to content

Commit 5df0d07

Browse files
committed
feat(ui): add custom shell mode switch to run command
1 parent 3518fde commit 5df0d07

3 files changed

Lines changed: 259 additions & 76 deletions

File tree

apps/dokploy/components/dashboard/application/advanced/general/add-command.tsx

Lines changed: 86 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
2-
import { Plus, Trash2 } from "lucide-react";
32
import { useEffect } from "react";
43
import { useFieldArray, useForm } from "react-hook-form";
54
import { toast } from "sonner";
@@ -20,25 +19,40 @@ import {
2019
FormLabel,
2120
FormMessage,
2221
} from "@/components/ui/form";
23-
import { Input } from "@/components/ui/input";
22+
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
2423
import { api } from "@/utils/api";
24+
import { CustomShellFields } from "./custom-shell-fields";
25+
import { SingleCommandFields } from "./single-command-fields";
2526

2627
interface Props {
2728
applicationId: string;
2829
}
2930

30-
const AddRedirectSchema = z.object({
31-
command: z.string(),
32-
args: z
33-
.array(
34-
z.object({
35-
value: z.string().min(1, "Argument cannot be empty"),
36-
}),
37-
)
38-
.optional(),
39-
});
31+
const AddCommandSchema = z
32+
.object({
33+
commandMode: z.enum(["single", "custom"]),
34+
command: z.string(),
35+
customCommand: z.string().max(20000).optional(),
36+
customShell: z.enum(["sh", "bash"]).optional(),
37+
args: z
38+
.array(
39+
z.object({
40+
value: z.string().min(1, "Argument cannot be empty"),
41+
}),
42+
)
43+
.optional(),
44+
})
45+
.superRefine((data, ctx) => {
46+
if (data.commandMode === "custom" && !data.customCommand?.trim()) {
47+
ctx.addIssue({
48+
code: "custom",
49+
path: ["customCommand"],
50+
message: "Enter a script",
51+
});
52+
}
53+
});
4054

41-
type AddCommand = z.infer<typeof AddRedirectSchema>;
55+
export type AddCommandForm = z.infer<typeof AddCommandSchema>;
4256

4357
export const AddCommand = ({ applicationId }: Props) => {
4458
const { data } = api.application.one.useQuery(
@@ -52,33 +66,48 @@ export const AddCommand = ({ applicationId }: Props) => {
5266

5367
const { mutateAsync, isPending } = api.application.update.useMutation();
5468

55-
const form = useForm<AddCommand>({
69+
const form = useForm<AddCommandForm>({
5670
defaultValues: {
71+
commandMode: "single",
5772
command: "",
73+
customCommand: "",
74+
customShell: "sh",
5875
args: [],
5976
},
60-
resolver: zodResolver(AddRedirectSchema),
77+
resolver: zodResolver(AddCommandSchema),
6178
});
6279

6380
const { fields, append, remove } = useFieldArray({
6481
control: form.control,
6582
name: "args",
6683
});
6784

85+
const commandMode = form.watch("commandMode");
86+
const customCommand = form.watch("customCommand");
87+
6888
useEffect(() => {
6989
if (data) {
7090
form.reset({
91+
commandMode: data?.customCommand ? "custom" : "single",
7192
command: data?.command || "",
93+
customCommand: data?.customCommand || "",
94+
customShell: data?.customShell === "bash" ? "bash" : "sh",
7295
args: data?.args?.map((arg) => ({ value: arg })) || [],
7396
});
7497
}
7598
}, [data, form]);
7699

77-
const onSubmit = async (data: AddCommand) => {
100+
const onSubmit = async (values: AddCommandForm) => {
78101
await mutateAsync({
79102
applicationId,
80-
command: data?.command,
81-
args: data?.args?.map((arg) => arg.value).filter(Boolean),
103+
command: values?.command,
104+
args: values?.args?.map((arg) => arg.value).filter(Boolean),
105+
customCommand:
106+
values.commandMode === "custom"
107+
? values.customCommand?.trim() || null
108+
: null,
109+
customShell:
110+
values.commandMode === "custom" ? (values.customShell ?? "sh") : null,
82111
})
83112
.then(async () => {
84113
toast.success("Command Updated");
@@ -111,73 +140,54 @@ export const AddCommand = ({ applicationId }: Props) => {
111140
<div className="flex flex-col gap-4">
112141
<FormField
113142
control={form.control}
114-
name="command"
143+
name="commandMode"
115144
render={({ field }) => (
116-
<FormItem>
117-
<FormLabel>Command</FormLabel>
145+
<FormItem className="space-y-3">
146+
<FormLabel>Mode</FormLabel>
118147
<FormControl>
119-
<Input placeholder="/bin/sh" {...field} />
148+
<RadioGroup
149+
onValueChange={field.onChange}
150+
value={field.value}
151+
className="flex flex-row gap-6"
152+
>
153+
<FormItem className="flex items-center space-x-2 space-y-0">
154+
<FormControl>
155+
<RadioGroupItem value="single" />
156+
</FormControl>
157+
<FormLabel className="font-normal">Single</FormLabel>
158+
</FormItem>
159+
<FormItem className="flex items-center space-x-2 space-y-0">
160+
<FormControl>
161+
<RadioGroupItem value="custom" />
162+
</FormControl>
163+
<FormLabel className="font-normal">
164+
Custom shell
165+
</FormLabel>
166+
</FormItem>
167+
</RadioGroup>
120168
</FormControl>
121-
122169
<FormMessage />
123170
</FormItem>
124171
)}
125172
/>
126-
127-
<div className="space-y-2">
128-
<div className="flex items-center justify-between">
129-
<FormLabel>Arguments (Args)</FormLabel>
130-
<Button
131-
type="button"
132-
variant="outline"
133-
size="sm"
134-
onClick={() => append({ value: "" })}
135-
>
136-
<Plus className="h-4 w-4 mr-1" />
137-
Add Argument
138-
</Button>
139-
</div>
140-
141-
{fields.length === 0 && (
142-
<p className="text-sm text-muted-foreground">
143-
No arguments added yet. Click "Add Argument" to add one.
144-
</p>
145-
)}
146-
147-
{fields.map((field, index) => (
148-
<FormField
149-
key={field.id}
150-
control={form.control}
151-
name={`args.${index}.value`}
152-
render={({ field }) => (
153-
<FormItem>
154-
<div className="flex gap-2">
155-
<FormControl>
156-
<Input
157-
placeholder={
158-
index === 0 ? "-c" : "echo Hello World"
159-
}
160-
{...field}
161-
/>
162-
</FormControl>
163-
<Button
164-
type="button"
165-
variant="destructive"
166-
size="icon"
167-
onClick={() => remove(index)}
168-
>
169-
<Trash2 className="h-4 w-4" />
170-
</Button>
171-
</div>
172-
<FormMessage />
173-
</FormItem>
174-
)}
175-
/>
176-
))}
177-
</div>
173+
{commandMode === "custom" ? (
174+
<CustomShellFields control={form.control} />
175+
) : (
176+
<SingleCommandFields
177+
control={form.control}
178+
fields={fields}
179+
append={append}
180+
remove={remove}
181+
/>
182+
)}
178183
</div>
179184
<div className="flex justify-end">
180-
<Button isLoading={isPending} type="submit" className="w-fit">
185+
<Button
186+
isLoading={isPending}
187+
type="submit"
188+
className="w-fit"
189+
disabled={commandMode === "custom" && !customCommand?.trim()}
190+
>
181191
Save
182192
</Button>
183193
</div>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { Control } from "react-hook-form";
2+
import {
3+
FormControl,
4+
FormField,
5+
FormItem,
6+
FormLabel,
7+
FormMessage,
8+
} from "@/components/ui/form";
9+
import {
10+
Select,
11+
SelectContent,
12+
SelectItem,
13+
SelectTrigger,
14+
SelectValue,
15+
} from "@/components/ui/select";
16+
import { Textarea } from "@/components/ui/textarea";
17+
import type { AddCommandForm } from "./add-command";
18+
19+
interface CustomShellFieldsProps {
20+
control: Control<AddCommandForm>;
21+
}
22+
23+
export const CustomShellFields = ({ control }: CustomShellFieldsProps) => {
24+
return (
25+
<>
26+
<FormField
27+
control={control}
28+
name="customShell"
29+
render={({ field }) => (
30+
<FormItem>
31+
<FormLabel>Shell</FormLabel>
32+
<Select onValueChange={field.onChange} value={field.value ?? "sh"}>
33+
<FormControl>
34+
<SelectTrigger>
35+
<SelectValue placeholder="Select a shell" />
36+
</SelectTrigger>
37+
</FormControl>
38+
<SelectContent>
39+
<SelectItem value="sh">sh</SelectItem>
40+
<SelectItem value="bash">bash</SelectItem>
41+
</SelectContent>
42+
</Select>
43+
<FormMessage />
44+
</FormItem>
45+
)}
46+
/>
47+
<FormField
48+
control={control}
49+
name="customCommand"
50+
render={({ field }) => (
51+
<FormItem>
52+
<FormLabel>Script</FormLabel>
53+
<FormControl>
54+
<Textarea
55+
placeholder="npx prisma migrate deploy && node server.js"
56+
className="font-mono"
57+
{...field}
58+
value={field.value ?? ""}
59+
/>
60+
</FormControl>
61+
<p className="text-sm text-muted-foreground">
62+
Example: php artisan migrate --force && apache2-foreground
63+
</p>
64+
<FormMessage />
65+
</FormItem>
66+
)}
67+
/>
68+
<p className="text-sm text-muted-foreground">
69+
Dokploy ignores Args in this mode. Stored args stay saved.
70+
</p>
71+
</>
72+
);
73+
};

0 commit comments

Comments
 (0)