mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-14 02:15:23 +02:00
- Added a check to prevent empty values from being processed in the onValueChange handler for Bitbucket, Gitea, GitHub, and GitLab providers. - Removed unnecessary defaultValue prop from Select components to streamline the code. - Updated button styles to remove background color for better consistency across the UI. - Enhanced volume backup selection to display a message when no volumes are found. This update enhances user experience by ensuring that empty selections are handled gracefully and improves the overall visual consistency of the UI components.
558 lines
16 KiB
TypeScript
558 lines
16 KiB
TypeScript
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
|
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useEffect } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import { cn } from "@/lib/utils";
|
|
import { api } from "@/utils/api";
|
|
|
|
const GithubProviderSchema = z.object({
|
|
composePath: z.string().min(1),
|
|
repository: z
|
|
.object({
|
|
repo: z.string().min(1, "Repo is required"),
|
|
owner: z.string().min(1, "Owner is required"),
|
|
})
|
|
.required(),
|
|
branch: z
|
|
.string()
|
|
.min(1, "Branch is required")
|
|
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
githubId: z.string().min(1, "Github Provider is required"),
|
|
watchPaths: z.array(z.string()).optional(),
|
|
triggerType: z.enum(["push", "tag"]).default("push"),
|
|
enableSubmodules: z.boolean().default(false),
|
|
});
|
|
|
|
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
|
|
|
interface Props {
|
|
composeId: string;
|
|
}
|
|
|
|
export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
|
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
|
|
|
const { mutateAsync, isPending: isSavingGithubProvider } =
|
|
api.compose.update.useMutation();
|
|
|
|
const form = useForm({
|
|
defaultValues: {
|
|
composePath: "./docker-compose.yml",
|
|
repository: {
|
|
owner: "",
|
|
repo: "",
|
|
},
|
|
githubId: "",
|
|
branch: "",
|
|
watchPaths: [],
|
|
triggerType: "push",
|
|
enableSubmodules: false,
|
|
},
|
|
resolver: zodResolver(GithubProviderSchema),
|
|
});
|
|
|
|
const repository = form.watch("repository");
|
|
const githubId = form.watch("githubId");
|
|
const triggerType = form.watch("triggerType");
|
|
const { data: repositories, isPending: isLoadingRepositories } =
|
|
api.github.getGithubRepositories.useQuery(
|
|
{
|
|
githubId,
|
|
},
|
|
{
|
|
enabled: !!githubId,
|
|
},
|
|
);
|
|
|
|
const {
|
|
data: branches,
|
|
fetchStatus,
|
|
status,
|
|
} = api.github.getGithubBranches.useQuery(
|
|
{
|
|
owner: repository?.owner,
|
|
repo: repository?.repo,
|
|
githubId,
|
|
},
|
|
{
|
|
enabled: !!repository?.owner && !!repository?.repo && !!githubId,
|
|
},
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
form.reset({
|
|
branch: data.branch || "",
|
|
repository: {
|
|
repo: data.repository || "",
|
|
owner: data.owner || "",
|
|
},
|
|
composePath: data.composePath,
|
|
githubId: data.githubId || "",
|
|
watchPaths: data.watchPaths || [],
|
|
triggerType: data.triggerType || "push",
|
|
enableSubmodules: data.enableSubmodules ?? false,
|
|
});
|
|
}
|
|
}, [form.reset, data]);
|
|
|
|
const onSubmit = async (data: GithubProvider) => {
|
|
await mutateAsync({
|
|
branch: data.branch,
|
|
repository: data.repository.repo,
|
|
composeId,
|
|
owner: data.repository.owner,
|
|
composePath: data.composePath,
|
|
githubId: data.githubId,
|
|
sourceType: "github",
|
|
composeStatus: "idle",
|
|
watchPaths: data.watchPaths,
|
|
enableSubmodules: data.enableSubmodules,
|
|
triggerType: data.triggerType,
|
|
})
|
|
.then(async () => {
|
|
toast.success("Service Provider Saved");
|
|
await refetch();
|
|
})
|
|
.catch(() => {
|
|
toast.error("Error saving the Github provider");
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="grid w-full gap-4 py-3"
|
|
>
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="githubId"
|
|
render={({ field }) => (
|
|
<FormItem className="md:col-span-2 flex flex-col">
|
|
<FormLabel>Github Account</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
field.onChange(value);
|
|
form.setValue("repository", {
|
|
owner: "",
|
|
repo: "",
|
|
});
|
|
form.setValue("branch", "");
|
|
}}
|
|
value={field.value}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a Github Account" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{githubProviders?.map((githubProvider) => (
|
|
<SelectItem
|
|
key={githubProvider.githubId}
|
|
value={githubProvider.githubId}
|
|
>
|
|
{githubProvider.gitProvider.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="repository"
|
|
render={({ field }) => (
|
|
<FormItem className="md:col-span-2 flex flex-col">
|
|
<div className="flex items-center justify-between">
|
|
<FormLabel>Repository</FormLabel>
|
|
{field.value.owner && field.value.repo && (
|
|
<Link
|
|
href={`https://github.com/${field.value.owner}/${field.value.repo}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
|
>
|
|
<GithubIcon className="h-4 w-4" />
|
|
<span>View Repository</span>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<FormControl>
|
|
<Button
|
|
variant="outline"
|
|
className={cn(
|
|
"w-full justify-between",
|
|
!field.value && "text-muted-foreground",
|
|
)}
|
|
>
|
|
{!field.value.owner
|
|
? "Select repository"
|
|
: isLoadingRepositories
|
|
? "Loading...."
|
|
: (repositories?.find(
|
|
(repo) => repo.name === field.value.repo,
|
|
)?.name ?? field.value.repo)}
|
|
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</FormControl>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start">
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="Search repository..."
|
|
className="h-9"
|
|
/>
|
|
{!githubId ? (
|
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
|
Select a GitHub account first
|
|
</span>
|
|
) : isLoadingRepositories ? (
|
|
<span className="py-6 text-center text-sm">
|
|
Loading Repositories....
|
|
</span>
|
|
) : null}
|
|
<CommandEmpty>No repositories found.</CommandEmpty>
|
|
<ScrollArea className="h-96">
|
|
<CommandGroup>
|
|
{repositories?.map((repo) => (
|
|
<CommandItem
|
|
value={repo.name}
|
|
key={repo.url}
|
|
onSelect={() => {
|
|
form.setValue("repository", {
|
|
owner: repo.owner.login as string,
|
|
repo: repo.name,
|
|
});
|
|
form.setValue("branch", "");
|
|
}}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<span>{repo.name}</span>
|
|
<span className="text-muted-foreground text-xs">
|
|
{repo.owner.login}
|
|
</span>
|
|
</span>
|
|
<CheckIcon
|
|
className={cn(
|
|
"ml-auto h-4 w-4",
|
|
repo.name === field.value.repo
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</ScrollArea>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
{form.formState.errors.repository && (
|
|
<p className={cn("text-sm font-medium text-destructive")}>
|
|
Repository is required
|
|
</p>
|
|
)}
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="branch"
|
|
render={({ field }) => (
|
|
<FormItem className="block w-full">
|
|
<FormLabel>Branch</FormLabel>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<FormControl>
|
|
<Button
|
|
variant="outline"
|
|
className={cn(
|
|
" w-full justify-between",
|
|
!field.value && "text-muted-foreground",
|
|
)}
|
|
>
|
|
{status === "pending" && fetchStatus === "fetching"
|
|
? "Loading...."
|
|
: field.value
|
|
? (branches?.find(
|
|
(branch) => branch.name === field.value,
|
|
)?.name ?? field.value)
|
|
: "Select branch"}
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</FormControl>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" align="start">
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="Search branch..."
|
|
className="h-9"
|
|
/>
|
|
{status === "pending" && fetchStatus === "fetching" && (
|
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
|
Loading Branches....
|
|
</span>
|
|
)}
|
|
{!repository?.owner && (
|
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
|
Select a repository
|
|
</span>
|
|
)}
|
|
<ScrollArea className="h-96">
|
|
<CommandEmpty>No branch found.</CommandEmpty>
|
|
|
|
<CommandGroup>
|
|
{branches?.map((branch) => (
|
|
<CommandItem
|
|
value={branch.name}
|
|
key={branch.commit.sha}
|
|
onSelect={() => {
|
|
form.setValue("branch", branch.name);
|
|
}}
|
|
>
|
|
{branch.name}
|
|
<CheckIcon
|
|
className={cn(
|
|
"ml-auto h-4 w-4",
|
|
branch.name === field.value
|
|
? "opacity-100"
|
|
: "opacity-0",
|
|
)}
|
|
/>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</ScrollArea>
|
|
</Command>
|
|
</PopoverContent>
|
|
|
|
<FormMessage />
|
|
</Popover>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="composePath"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Compose Path</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="docker-compose.yml" {...field} />
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="triggerType"
|
|
render={({ field }) => (
|
|
<FormItem className="md:col-span-2">
|
|
<div className="flex items-center gap-2">
|
|
<FormLabel>Trigger Type</FormLabel>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>
|
|
Choose when to trigger deployments: on push to the
|
|
selected branch or when a new tag is created.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
defaultValue={field.value}
|
|
value={field.value}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a trigger type" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="push">On Push</SelectItem>
|
|
<SelectItem value="tag">On Tag</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
{triggerType === "push" && (
|
|
<FormField
|
|
control={form.control}
|
|
name="watchPaths"
|
|
render={({ field }) => (
|
|
<FormItem className="md:col-span-2">
|
|
<div className="flex items-center gap-2">
|
|
<FormLabel>Watch Paths</FormLabel>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger type="button">
|
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
|
?
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>
|
|
Add paths to watch for changes. When files in
|
|
these paths change, a new deployment will be
|
|
triggered.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
{field.value?.map((path, index) => (
|
|
<Badge key={index} variant="secondary">
|
|
{path}
|
|
<X
|
|
className="ml-1 size-3 cursor-pointer"
|
|
onClick={() => {
|
|
const newPaths = [...(field.value || [])];
|
|
newPaths.splice(index, 1);
|
|
form.setValue("watchPaths", newPaths);
|
|
}}
|
|
/>
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
<FormControl>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
const input = e.currentTarget;
|
|
const value = input.value.trim();
|
|
if (value) {
|
|
const newPaths = [
|
|
...(field.value || []),
|
|
value,
|
|
];
|
|
form.setValue("watchPaths", newPaths);
|
|
input.value = "";
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() => {
|
|
const input = document.querySelector(
|
|
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
|
) as HTMLInputElement;
|
|
const value = input.value.trim();
|
|
if (value) {
|
|
const newPaths = [...(field.value || []), value];
|
|
form.setValue("watchPaths", newPaths);
|
|
input.value = "";
|
|
}
|
|
}}
|
|
>
|
|
Add
|
|
</Button>
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
<FormField
|
|
control={form.control}
|
|
name="enableSubmodules"
|
|
render={({ field }) => (
|
|
<FormItem className="flex items-center space-x-2">
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="flex w-full justify-end">
|
|
<Button
|
|
isLoading={isSavingGithubProvider}
|
|
type="submit"
|
|
className="w-fit"
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
);
|
|
};
|