mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-11 22:45:18 +00: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.
522 lines
15 KiB
TypeScript
522 lines
15 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 { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
|
import { AlertBlock } from "@/components/shared/alert-block";
|
|
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 BitbucketProviderSchema = z.object({
|
|
buildPath: z.string().min(1, "Path is required").default("/"),
|
|
repository: z
|
|
.object({
|
|
repo: z.string().min(1, "Repo is required"),
|
|
owner: z.string().min(1, "Owner is required"),
|
|
slug: z.string().optional(),
|
|
})
|
|
.required(),
|
|
branch: z
|
|
.string()
|
|
.min(1, "Branch is required")
|
|
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
|
watchPaths: z.array(z.string()).optional(),
|
|
enableSubmodules: z.boolean().optional(),
|
|
});
|
|
|
|
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
|
|
|
interface Props {
|
|
applicationId: string;
|
|
}
|
|
|
|
export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|
const { data: bitbucketProviders } =
|
|
api.bitbucket.bitbucketProviders.useQuery();
|
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
|
|
|
const { mutateAsync, isPending: isSavingBitbucketProvider } =
|
|
api.application.saveBitbucketProvider.useMutation();
|
|
|
|
const form = useForm({
|
|
defaultValues: {
|
|
buildPath: "/",
|
|
repository: {
|
|
owner: "",
|
|
repo: "",
|
|
slug: "",
|
|
},
|
|
bitbucketId: "",
|
|
branch: "",
|
|
watchPaths: [],
|
|
enableSubmodules: false,
|
|
},
|
|
resolver: zodResolver(BitbucketProviderSchema),
|
|
});
|
|
|
|
const repository = form.watch("repository");
|
|
const bitbucketId = form.watch("bitbucketId");
|
|
|
|
const {
|
|
data: repositories,
|
|
isLoading: isLoadingRepositories,
|
|
error,
|
|
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
|
{
|
|
bitbucketId,
|
|
},
|
|
{
|
|
enabled: !!bitbucketId,
|
|
},
|
|
);
|
|
|
|
const {
|
|
data: branches,
|
|
fetchStatus,
|
|
status,
|
|
} = api.bitbucket.getBitbucketBranches.useQuery(
|
|
{
|
|
owner: repository?.owner,
|
|
repo: repository?.slug || repository?.repo || "",
|
|
bitbucketId,
|
|
},
|
|
{
|
|
enabled:
|
|
!!repository?.owner &&
|
|
!!(repository?.slug || repository?.repo) &&
|
|
!!bitbucketId,
|
|
},
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
form.reset({
|
|
branch: data.bitbucketBranch || "",
|
|
repository: {
|
|
repo: data.bitbucketRepository || "",
|
|
owner: data.bitbucketOwner || "",
|
|
slug: data.bitbucketRepositorySlug || "",
|
|
},
|
|
buildPath: data.bitbucketBuildPath || "/",
|
|
bitbucketId: data.bitbucketId || "",
|
|
watchPaths: data.watchPaths || [],
|
|
enableSubmodules: data.enableSubmodules || false,
|
|
});
|
|
}
|
|
}, [form.reset, data?.applicationId, form]);
|
|
|
|
const onSubmit = async (data: BitbucketProvider) => {
|
|
await mutateAsync({
|
|
bitbucketBranch: data.branch,
|
|
bitbucketRepository: data.repository.repo,
|
|
bitbucketRepositorySlug: data.repository.slug || data.repository.repo,
|
|
bitbucketOwner: data.repository.owner,
|
|
bitbucketBuildPath: data.buildPath,
|
|
bitbucketId: data.bitbucketId,
|
|
applicationId,
|
|
watchPaths: data.watchPaths || [],
|
|
enableSubmodules: data.enableSubmodules || false,
|
|
})
|
|
.then(async () => {
|
|
toast.success("Service Provider Saved");
|
|
await refetch();
|
|
})
|
|
.catch(() => {
|
|
toast.error("Error saving the Bitbucket provider");
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="grid w-full gap-4 py-3"
|
|
>
|
|
{error && (
|
|
<AlertBlock type="error">Repositories: {error.message}</AlertBlock>
|
|
)}
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="bitbucketId"
|
|
render={({ field }) => (
|
|
<FormItem className="md:col-span-2 flex flex-col">
|
|
<FormLabel>Bitbucket Account</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
field.onChange(value);
|
|
form.setValue("repository", {
|
|
owner: "",
|
|
repo: "",
|
|
slug: "",
|
|
});
|
|
form.setValue("branch", "");
|
|
}}
|
|
value={field.value}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a Bitbucket Account" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{bitbucketProviders?.map((bitbucketProvider) => (
|
|
<SelectItem
|
|
key={bitbucketProvider.bitbucketId}
|
|
value={bitbucketProvider.bitbucketId}
|
|
>
|
|
{bitbucketProvider.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://bitbucket.org/${field.value.owner}/${field.value.slug || field.value.repo}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
|
>
|
|
<BitbucketIcon 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 ?? "Select repository")}
|
|
|
|
<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"
|
|
/>
|
|
{!bitbucketId ? (
|
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
|
Select a Bitbucket 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.username as string,
|
|
repo: repo.name,
|
|
slug: repo.slug,
|
|
});
|
|
form.setValue("branch", "");
|
|
}}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<span>{repo.name}</span>
|
|
<span className="text-muted-foreground text-xs">
|
|
{repo.owner.username}
|
|
</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
|
|
: "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="buildPath"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Build Path</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="/" {...field} />
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<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 asChild>
|
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
|
</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={isSavingBitbucketProvider}
|
|
type="submit"
|
|
className="w-fit"
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
);
|
|
};
|