mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-21 13:55:33 +02:00
Updated the PopoverContent components across various provider files to ensure consistent width and padding. Enhanced the display of repository and branch names by adding truncation to prevent overflow in the UI. This change applies to Bitbucket, Gitea, GitHub, and GitLab provider components, as well as their respective compose components.
550 lines
16 KiB
TypeScript
550 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, Plus, X } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useEffect, useMemo } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { toast } from "sonner";
|
|
import { z } from "zod";
|
|
import { GitlabIcon } 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 GitlabProviderSchema = 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"),
|
|
gitlabPathNamespace: z.string().min(1),
|
|
id: z.number().nullable(),
|
|
})
|
|
.required(),
|
|
branch: z
|
|
.string()
|
|
.min(1, "Branch is required")
|
|
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
|
watchPaths: z.array(z.string()).optional(),
|
|
enableSubmodules: z.boolean().default(false),
|
|
});
|
|
|
|
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
|
|
|
interface Props {
|
|
applicationId: string;
|
|
}
|
|
|
|
export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
|
|
|
const { mutateAsync, isPending: isSavingGitlabProvider } =
|
|
api.application.saveGitlabProvider.useMutation();
|
|
|
|
const form = useForm({
|
|
defaultValues: {
|
|
buildPath: "/",
|
|
repository: {
|
|
owner: "",
|
|
repo: "",
|
|
gitlabPathNamespace: "",
|
|
id: null,
|
|
},
|
|
gitlabId: "",
|
|
branch: "",
|
|
enableSubmodules: false,
|
|
},
|
|
resolver: zodResolver(GitlabProviderSchema),
|
|
});
|
|
|
|
const repository = form.watch("repository");
|
|
const gitlabId = form.watch("gitlabId");
|
|
|
|
const gitlabUrl = useMemo(() => {
|
|
const url = gitlabProviders?.find(
|
|
(provider) => provider.gitlabId === gitlabId,
|
|
)?.gitlabUrl;
|
|
|
|
const gitlabUrl = url?.replace(/\/$/, "");
|
|
|
|
return gitlabUrl || "https://gitlab.com";
|
|
}, [gitlabId, gitlabProviders]);
|
|
|
|
const {
|
|
data: repositories,
|
|
isLoading: isLoadingRepositories,
|
|
error,
|
|
} = api.gitlab.getGitlabRepositories.useQuery(
|
|
{
|
|
gitlabId,
|
|
},
|
|
{
|
|
enabled: !!gitlabId,
|
|
},
|
|
);
|
|
|
|
const {
|
|
data: branches,
|
|
fetchStatus,
|
|
status,
|
|
} = api.gitlab.getGitlabBranches.useQuery(
|
|
{
|
|
owner: repository?.owner,
|
|
repo: repository?.repo,
|
|
id: repository?.id || 0,
|
|
gitlabId: gitlabId,
|
|
},
|
|
{
|
|
enabled: !!repository?.owner && !!repository?.repo && !!gitlabId,
|
|
},
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (data) {
|
|
form.reset({
|
|
branch: data.gitlabBranch || "",
|
|
repository: {
|
|
repo: data.gitlabRepository || "",
|
|
owner: data.gitlabOwner || "",
|
|
gitlabPathNamespace: data.gitlabPathNamespace || "",
|
|
id: data.gitlabProjectId,
|
|
},
|
|
buildPath: data.gitlabBuildPath || "/",
|
|
gitlabId: data.gitlabId || "",
|
|
watchPaths: data.watchPaths || [],
|
|
enableSubmodules: data.enableSubmodules ?? false,
|
|
});
|
|
}
|
|
}, [form.reset, data?.applicationId, form]);
|
|
|
|
const onSubmit = async (data: GitlabProvider) => {
|
|
await mutateAsync({
|
|
gitlabBranch: data.branch,
|
|
gitlabRepository: data.repository.repo,
|
|
gitlabOwner: data.repository.owner,
|
|
gitlabBuildPath: data.buildPath,
|
|
gitlabId: data.gitlabId,
|
|
applicationId,
|
|
gitlabProjectId: data.repository.id,
|
|
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
|
watchPaths: data.watchPaths || [],
|
|
enableSubmodules: data.enableSubmodules,
|
|
})
|
|
.then(async () => {
|
|
toast.success("Service Provider Saved");
|
|
await refetch();
|
|
})
|
|
.catch(() => {
|
|
toast.error("Error saving the gitlab provider");
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Form {...form}>
|
|
<form
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
className="grid w-full gap-4 py-3"
|
|
>
|
|
{error && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="gitlabId"
|
|
render={({ field }) => (
|
|
<FormItem className="md:col-span-2 flex flex-col">
|
|
<FormLabel>Gitlab Account</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
field.onChange(value);
|
|
form.setValue("repository", {
|
|
owner: "",
|
|
repo: "",
|
|
id: null,
|
|
gitlabPathNamespace: "",
|
|
});
|
|
form.setValue("branch", "");
|
|
}}
|
|
value={field.value}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a Gitlab Account" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{gitlabProviders?.map((gitlabProvider) => (
|
|
<SelectItem
|
|
key={gitlabProvider.gitlabId}
|
|
value={gitlabProvider.gitlabId}
|
|
>
|
|
{gitlabProvider.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.gitlabPathNamespace && (
|
|
<Link
|
|
href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
|
>
|
|
<GitlabIcon 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="w-[var(--radix-popover-trigger-width)] p-0"
|
|
align="start"
|
|
>
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="Search repository..."
|
|
className="h-9"
|
|
/>
|
|
{!gitlabId ? (
|
|
<span className="py-6 text-center text-sm text-muted-foreground">
|
|
Select a GitLab 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 && repositories.length === 0 && (
|
|
<CommandEmpty>
|
|
No repositories found.
|
|
</CommandEmpty>
|
|
)}
|
|
{repositories?.map((repo) => {
|
|
return (
|
|
<CommandItem
|
|
value={repo.url}
|
|
key={repo.url}
|
|
onSelect={() => {
|
|
form.setValue("repository", {
|
|
owner: repo.owner.username as string,
|
|
repo: repo.name,
|
|
id: repo.id,
|
|
gitlabPathNamespace: repo.url,
|
|
});
|
|
form.setValue("branch", "");
|
|
}}
|
|
>
|
|
<span className="flex min-w-0 items-center gap-2">
|
|
<span className="truncate">
|
|
{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.url ===
|
|
field.value.gitlabPathNamespace
|
|
? "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="w-[var(--radix-popover-trigger-width)] 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.id}
|
|
onSelect={() => {
|
|
form.setValue("branch", branch.name);
|
|
}}
|
|
>
|
|
<span className="truncate">{branch.name}</span>
|
|
<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={`${path}-${index}`}
|
|
variant="secondary"
|
|
className="flex items-center gap-1"
|
|
>
|
|
{path}
|
|
<X
|
|
className="size-3 cursor-pointer hover:text-destructive"
|
|
onClick={() => {
|
|
const newPaths = [...(field.value || [])];
|
|
newPaths.splice(index, 1);
|
|
field.onChange(newPaths);
|
|
}}
|
|
/>
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<FormControl>
|
|
<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 path = input.value.trim();
|
|
if (path) {
|
|
field.onChange([...(field.value || []), path]);
|
|
input.value = "";
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => {
|
|
const input = document.querySelector(
|
|
'input[placeholder*="Enter a path"]',
|
|
) as HTMLInputElement;
|
|
const path = input.value.trim();
|
|
if (path) {
|
|
field.onChange([...(field.value || []), path]);
|
|
input.value = "";
|
|
}
|
|
}}
|
|
>
|
|
<Plus className="size-4" />
|
|
</Button>
|
|
</div>
|
|
<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={isSavingGitlabProvider}
|
|
type="submit"
|
|
className="w-fit"
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
);
|
|
};
|