Skip to content

Commit 0372372

Browse files
authored
Merge pull request #1443 from Dokploy/873-can-monorepo-autoploy-base-on-different-paths
feat(applications): add watch paths for selective deployments
2 parents 1ad25ca + 492d513 commit 0372372

27 files changed

+11422
-38
lines changed

apps/dokploy/__test__/drop/drop.test.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ if (typeof window === "undefined") {
2727
const baseApp: ApplicationNested = {
2828
applicationId: "",
2929
herokuVersion: "",
30+
watchPaths: [],
3031
applicationStatus: "done",
3132
appName: "",
3233
autoDeploy: true,

apps/dokploy/__test__/traefik/traefik.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const baseApp: ApplicationNested = {
1414
branch: null,
1515
dockerBuildStage: "",
1616
registryUrl: "",
17+
watchPaths: [],
1718
buildArgs: null,
1819
isPreviewDeploymentsActive: false,
1920
previewBuildArgs: null,

apps/dokploy/components/dashboard/application/general/generic/save-bitbucket-provider.tsx

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,23 @@ import {
2929
SelectTrigger,
3030
SelectValue,
3131
} from "@/components/ui/select";
32+
import {
33+
Tooltip,
34+
TooltipContent,
35+
TooltipProvider,
36+
TooltipTrigger,
37+
} from "@/components/ui/tooltip";
3238
import { cn } from "@/lib/utils";
3339
import { api } from "@/utils/api";
3440
import { zodResolver } from "@hookform/resolvers/zod";
35-
import { CheckIcon, ChevronsUpDown } from "lucide-react";
41+
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
3642
import { useEffect } from "react";
3743
import { useForm } from "react-hook-form";
3844
import { toast } from "sonner";
3945
import { z } from "zod";
46+
import { Badge } from "@/components/ui/badge";
47+
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
48+
import Link from "next/link";
4049

4150
const BitbucketProviderSchema = z.object({
4251
buildPath: z.string().min(1, "Path is required").default("/"),
@@ -48,6 +57,7 @@ const BitbucketProviderSchema = z.object({
4857
.required(),
4958
branch: z.string().min(1, "Branch is required"),
5059
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
60+
watchPaths: z.array(z.string()).optional(),
5161
});
5262

5363
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
@@ -73,6 +83,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
7383
},
7484
bitbucketId: "",
7585
branch: "",
86+
watchPaths: [],
7687
},
7788
resolver: zodResolver(BitbucketProviderSchema),
7889
});
@@ -118,6 +129,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
118129
},
119130
buildPath: data.bitbucketBuildPath || "/",
120131
bitbucketId: data.bitbucketId || "",
132+
watchPaths: data.watchPaths || [],
121133
});
122134
}
123135
}, [form.reset, data, form]);
@@ -130,6 +142,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
130142
bitbucketBuildPath: data.buildPath,
131143
bitbucketId: data.bitbucketId,
132144
applicationId,
145+
watchPaths: data.watchPaths || [],
133146
})
134147
.then(async () => {
135148
toast.success("Service Provided Saved");
@@ -195,7 +208,20 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
195208
name="repository"
196209
render={({ field }) => (
197210
<FormItem className="md:col-span-2 flex flex-col">
198-
<FormLabel>Repository</FormLabel>
211+
<div className="flex items-center justify-between">
212+
<FormLabel>Repository</FormLabel>
213+
{field.value.owner && field.value.repo && (
214+
<Link
215+
href={`https://bitbucket.org/${field.value.owner}/${field.value.repo}`}
216+
target="_blank"
217+
rel="noopener noreferrer"
218+
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
219+
>
220+
<BitbucketIcon className="h-4 w-4" />
221+
<span>View Repository</span>
222+
</Link>
223+
)}
224+
</div>
199225
<Popover>
200226
<PopoverTrigger asChild>
201227
<FormControl>
@@ -363,6 +389,84 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
363389
</FormItem>
364390
)}
365391
/>
392+
<FormField
393+
control={form.control}
394+
name="watchPaths"
395+
render={({ field }) => (
396+
<FormItem className="md:col-span-2">
397+
<div className="flex items-center gap-2">
398+
<FormLabel>Watch Paths</FormLabel>
399+
<TooltipProvider>
400+
<Tooltip>
401+
<TooltipTrigger>
402+
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
403+
?
404+
</div>
405+
</TooltipTrigger>
406+
<TooltipContent>
407+
<p>
408+
Add paths to watch for changes. When files in these
409+
paths change, a new deployment will be triggered.
410+
</p>
411+
</TooltipContent>
412+
</Tooltip>
413+
</TooltipProvider>
414+
</div>
415+
<div className="flex flex-wrap gap-2 mb-2">
416+
{field.value?.map((path, index) => (
417+
<Badge key={index} variant="secondary">
418+
{path}
419+
<X
420+
className="ml-1 size-3 cursor-pointer"
421+
onClick={() => {
422+
const newPaths = [...(field.value || [])];
423+
newPaths.splice(index, 1);
424+
form.setValue("watchPaths", newPaths);
425+
}}
426+
/>
427+
</Badge>
428+
))}
429+
</div>
430+
<FormControl>
431+
<div className="flex gap-2">
432+
<Input
433+
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
434+
onKeyDown={(e) => {
435+
if (e.key === "Enter") {
436+
e.preventDefault();
437+
const input = e.currentTarget;
438+
const value = input.value.trim();
439+
if (value) {
440+
const newPaths = [...(field.value || []), value];
441+
form.setValue("watchPaths", newPaths);
442+
input.value = "";
443+
}
444+
}
445+
}}
446+
/>
447+
<Button
448+
type="button"
449+
variant="secondary"
450+
onClick={() => {
451+
const input = document.querySelector(
452+
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
453+
) as HTMLInputElement;
454+
const value = input.value.trim();
455+
if (value) {
456+
const newPaths = [...(field.value || []), value];
457+
form.setValue("watchPaths", newPaths);
458+
input.value = "";
459+
}
460+
}}
461+
>
462+
Add
463+
</Button>
464+
</div>
465+
</FormControl>
466+
<FormMessage />
467+
</FormItem>
468+
)}
469+
/>
366470
</div>
367471
<div className="flex w-full justify-end">
368472
<Button

apps/dokploy/components/dashboard/application/general/generic/save-git-provider.tsx

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,33 @@ import {
1717
SelectTrigger,
1818
SelectValue,
1919
} from "@/components/ui/select";
20+
import {
21+
Tooltip,
22+
TooltipContent,
23+
TooltipProvider,
24+
TooltipTrigger,
25+
} from "@/components/ui/tooltip";
2026
import { api } from "@/utils/api";
2127
import { zodResolver } from "@hookform/resolvers/zod";
22-
import { KeyRoundIcon, LockIcon } from "lucide-react";
28+
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
2329
import { useRouter } from "next/router";
30+
import Link from "next/link";
2431

2532
import { useEffect } from "react";
2633
import { useForm } from "react-hook-form";
2734
import { toast } from "sonner";
2835
import { z } from "zod";
36+
import { Badge } from "@/components/ui/badge";
37+
import { GitIcon } from "@/components/icons/data-tools-icons";
2938

3039
const GitProviderSchema = z.object({
40+
buildPath: z.string().min(1, "Path is required").default("/"),
3141
repositoryURL: z.string().min(1, {
3242
message: "Repository URL is required",
3343
}),
3444
branch: z.string().min(1, "Branch required"),
35-
buildPath: z.string().min(1, "Build Path required"),
3645
sshKey: z.string().optional(),
46+
watchPaths: z.array(z.string()).optional(),
3747
});
3848

3949
type GitProvider = z.infer<typeof GitProviderSchema>;
@@ -56,6 +66,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
5666
buildPath: "/",
5767
repositoryURL: "",
5868
sshKey: undefined,
69+
watchPaths: [],
5970
},
6071
resolver: zodResolver(GitProviderSchema),
6172
});
@@ -67,6 +78,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
6778
branch: data.customGitBranch || "",
6879
buildPath: data.customGitBuildPath || "/",
6980
repositoryURL: data.customGitUrl || "",
81+
watchPaths: data.watchPaths || [],
7082
});
7183
}
7284
}, [form.reset, data, form]);
@@ -78,6 +90,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
7890
customGitUrl: values.repositoryURL,
7991
customGitSSHKeyId: values.sshKey === "none" ? null : values.sshKey,
8092
applicationId,
93+
watchPaths: values.watchPaths || [],
8194
})
8295
.then(async () => {
8396
toast.success("Git Provider Saved");
@@ -102,9 +115,22 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
102115
name="repositoryURL"
103116
render={({ field }) => (
104117
<FormItem>
105-
<FormLabel>Repository URL</FormLabel>
118+
<div className="flex items-center justify-between">
119+
<FormLabel>Repository URL</FormLabel>
120+
{field.value?.startsWith("https://") && (
121+
<Link
122+
href={field.value}
123+
target="_blank"
124+
rel="noopener noreferrer"
125+
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
126+
>
127+
<GitIcon className="h-4 w-4" />
128+
<span>View Repository</span>
129+
</Link>
130+
)}
131+
</div>
106132
<FormControl>
107-
<Input placeholder="[email protected]" {...field} />
133+
<Input placeholder="Repository URL" {...field} />
108134
</FormControl>
109135
<FormMessage />
110136
</FormItem>
@@ -160,27 +186,109 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
160186
</Button>
161187
)}
162188
</div>
189+
<div className="space-y-4">
190+
<FormField
191+
control={form.control}
192+
name="branch"
193+
render={({ field }) => (
194+
<FormItem>
195+
<FormLabel>Branch</FormLabel>
196+
<FormControl>
197+
<Input placeholder="Branch" {...field} />
198+
</FormControl>
199+
<FormMessage />
200+
</FormItem>
201+
)}
202+
/>
203+
</div>
204+
163205
<FormField
164206
control={form.control}
165-
name="branch"
207+
name="buildPath"
166208
render={({ field }) => (
167209
<FormItem>
168-
<FormLabel>Branch</FormLabel>
210+
<FormLabel>Build Path</FormLabel>
169211
<FormControl>
170-
<Input placeholder="Branch" {...field} />
212+
<Input placeholder="/" {...field} />
171213
</FormControl>
172214
<FormMessage />
173215
</FormItem>
174216
)}
175217
/>
176218
<FormField
177219
control={form.control}
178-
name="buildPath"
220+
name="watchPaths"
179221
render={({ field }) => (
180-
<FormItem>
181-
<FormLabel>Build Path</FormLabel>
222+
<FormItem className="md:col-span-2">
223+
<div className="flex items-center gap-2">
224+
<FormLabel>Watch Paths</FormLabel>
225+
<TooltipProvider>
226+
<Tooltip>
227+
<TooltipTrigger>
228+
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
229+
?
230+
</div>
231+
</TooltipTrigger>
232+
<TooltipContent className="max-w-[300px]">
233+
<p>
234+
Add paths to watch for changes. When files in these
235+
paths change, a new deployment will be triggered. This
236+
will work only when manual webhook is setup.
237+
</p>
238+
</TooltipContent>
239+
</Tooltip>
240+
</TooltipProvider>
241+
</div>
242+
<div className="flex flex-wrap gap-2 mb-2">
243+
{field.value?.map((path, index) => (
244+
<Badge key={index} variant="secondary">
245+
{path}
246+
<X
247+
className="ml-1 size-3 cursor-pointer"
248+
onClick={() => {
249+
const newPaths = [...(field.value || [])];
250+
newPaths.splice(index, 1);
251+
form.setValue("watchPaths", newPaths);
252+
}}
253+
/>
254+
</Badge>
255+
))}
256+
</div>
182257
<FormControl>
183-
<Input placeholder="/" {...field} />
258+
<div className="flex gap-2">
259+
<Input
260+
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
261+
onKeyDown={(e) => {
262+
if (e.key === "Enter") {
263+
e.preventDefault();
264+
const input = e.currentTarget;
265+
const value = input.value.trim();
266+
if (value) {
267+
const newPaths = [...(field.value || []), value];
268+
form.setValue("watchPaths", newPaths);
269+
input.value = "";
270+
}
271+
}
272+
}}
273+
/>
274+
<Button
275+
type="button"
276+
variant="secondary"
277+
onClick={() => {
278+
const input = document.querySelector(
279+
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
280+
) as HTMLInputElement;
281+
const value = input.value.trim();
282+
if (value) {
283+
const newPaths = [...(field.value || []), value];
284+
form.setValue("watchPaths", newPaths);
285+
input.value = "";
286+
}
287+
}}
288+
>
289+
Add
290+
</Button>
291+
</div>
184292
</FormControl>
185293
<FormMessage />
186294
</FormItem>

0 commit comments

Comments
 (0)