feat(providers): add gitlab bitbucket and github providers

This commit is contained in:
Mauricio Siu
2024-08-31 22:57:41 -06:00
parent 28d8fa9834
commit 6d945371c9
58 changed files with 46852 additions and 311 deletions
@@ -0,0 +1,374 @@
import { AlertBlock } from "@/components/shared/alert-block";
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 { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
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"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
bitbucketProviderId: z.string().min(1, "Bitbucket Provider is required"),
});
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
interface Props {
applicationId: string;
}
export const SaveBitbucketProvider = ({ applicationId }: Props) => {
const { data: bitbucketProviders } =
api.gitProvider.bitbucketProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
api.application.saveBitbucketProvider.useMutation();
const form = useForm<BitbucketProvider>({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
},
bitbucketProviderId: "",
branch: "",
},
resolver: zodResolver(BitbucketProviderSchema),
});
const repository = form.watch("repository");
const bitbucketProviderId = form.watch("bitbucketProviderId");
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
isError,
} = api.gitProvider.getBitbucketRepositories.useQuery({
bitbucketProviderId,
});
const {
data: branches,
fetchStatus,
status,
} = api.gitProvider.getBitbucketBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
bitbucketProviderId,
},
{
enabled:
!!repository?.owner && !!repository?.repo && !!bitbucketProviderId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.bitbucketBranch || "",
repository: {
repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "",
},
buildPath: data.bitbucketBuildPath || "/",
bitbucketProviderId: data.bitbucketProviderId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo,
bitbucketOwner: data.repository.owner,
bitbucketBuildPath: data.buildPath,
bitbucketProviderId: data.bitbucketProviderId,
applicationId,
})
.then(async () => {
toast.success("Service Provided Saved");
await refetch();
})
.catch(() => {
toast.error("Error to save 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="bitbucketProviderId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Bitbucket Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Bitbucket Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{bitbucketProviders?.map((bitbucketProvider) => (
<SelectItem
key={bitbucketProvider.bitbucketProviderId}
value={bitbucketProvider.bitbucketProviderId}
>
{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">
<FormLabel>Repository</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? 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"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories?.map((repo) => (
<CommandItem
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
{repo.name}
<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"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && 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 === "loading" && 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>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingBitbucketProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};
@@ -21,6 +21,13 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -39,6 +46,7 @@ const GithubProviderSchema = z.object({
})
.required(),
branch: z.string().min(1, "Branch is required"),
githubProviderId: z.string().min(1, "Github Provider is required"),
});
type GithubProvider = z.infer<typeof GithubProviderSchema>;
@@ -48,6 +56,7 @@ interface Props {
}
export const SaveGithubProvider = ({ applicationId }: Props) => {
const { data: githubProviders } = api.gitProvider.githubProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGithubProvider } =
@@ -60,15 +69,19 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
owner: "",
repo: "",
},
githubProviderId: "",
branch: "",
},
resolver: zodResolver(GithubProviderSchema),
});
const repository = form.watch("repository");
const githubProviderId = form.watch("githubProviderId");
const { data: repositories, isLoading: isLoadingRepositories } =
api.admin.getRepositories.useQuery();
api.admin.getRepositories.useQuery({
githubProviderId,
});
const {
data: branches,
@@ -78,8 +91,11 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
{
owner: repository?.owner,
repo: repository?.repo,
githubProviderId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!githubProviderId,
},
{ enabled: !!repository?.owner && !!repository?.repo },
);
useEffect(() => {
@@ -91,6 +107,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
owner: data.owner || "",
},
buildPath: data.buildPath || "/",
githubProviderId: data.githubProviderId || "",
});
}
}, [form.reset, data, form]);
@@ -102,6 +119,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
applicationId,
owner: data.repository.owner,
buildPath: data.buildPath,
githubProviderId: data.githubProviderId,
})
.then(async () => {
toast.success("Service Provided Saved");
@@ -120,6 +138,45 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
className="grid w-full gap-4 py-3"
>
<div className="grid md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="githubProviderId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Github Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Github Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{githubProviders?.map((githubProvider) => (
<SelectItem
key={githubProvider.githubProviderId}
value={githubProvider.githubProviderId}
>
{githubProvider.gitProvider.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repository"
@@ -0,0 +1,371 @@
import { AlertBlock } from "@/components/shared/alert-block";
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 { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
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"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
gitlabProviderId: z.string().min(1, "Gitlab Provider is required"),
});
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
interface Props {
applicationId: string;
}
export const SaveGitlabProvider = ({ applicationId }: Props) => {
const { data: gitlabProviders } = api.gitProvider.gitlabProviders.useQuery();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync, isLoading: isSavingGitlabProvider } =
api.application.saveGitlabProvider.useMutation();
const form = useForm<GitlabProvider>({
defaultValues: {
buildPath: "/",
repository: {
owner: "",
repo: "",
},
gitlabProviderId: "",
branch: "",
},
resolver: zodResolver(GitlabProviderSchema),
});
const repository = form.watch("repository");
const gitlabProviderId = form.watch("gitlabProviderId");
const {
data: repositories,
isLoading: isLoadingRepositories,
error,
} = api.gitProvider.getGitlabRepositories.useQuery({
gitlabProviderId,
});
const {
data: branches,
fetchStatus,
status,
} = api.gitProvider.getGitlabBranches.useQuery(
{
owner: repository?.owner,
repo: repository?.repo,
gitlabProviderId: gitlabProviderId,
},
{
enabled: !!repository?.owner && !!repository?.repo && !!gitlabProviderId,
},
);
useEffect(() => {
if (data) {
form.reset({
branch: data.gitlabBranch || "",
repository: {
repo: data.gitlabRepository || "",
owner: data.gitlabOwner || "",
},
buildPath: data.gitlabBuildPath || "/",
gitlabProviderId: data.gitlabProviderId || "",
});
}
}, [form.reset, data, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
gitlabBranch: data.branch,
gitlabRepository: data.repository.repo,
gitlabOwner: data.repository.owner,
gitlabBuildPath: data.buildPath,
gitlabProviderId: data.gitlabProviderId,
applicationId,
})
.then(async () => {
toast.success("Service Provided Saved");
await refetch();
})
.catch(() => {
toast.error("Error to save 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="gitlabProviderId"
render={({ field }) => (
<FormItem className="md:col-span-2 flex flex-col">
<FormLabel>Gitlab Account</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
form.setValue("repository", {
owner: "",
repo: "",
});
form.setValue("branch", "");
}}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a Gitlab Account" />
</SelectTrigger>
</FormControl>
<SelectContent>
{gitlabProviders?.map((gitlabProvider) => (
<SelectItem
key={gitlabProvider.gitlabProviderId}
value={gitlabProvider.gitlabProviderId}
>
{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">
<FormLabel>Repository</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{isLoadingRepositories
? "Loading...."
: field.value.owner
? 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"
/>
{isLoadingRepositories && (
<span className="py-6 text-center text-sm">
Loading Repositories....
</span>
)}
<CommandEmpty>No repositories found.</CommandEmpty>
<ScrollArea className="h-96">
<CommandGroup>
{repositories?.map((repo) => {
return (
<CommandItem
value={repo.url}
key={repo.url}
onSelect={() => {
form.setValue("repository", {
owner: repo.owner.username as string,
repo: repo.name,
});
form.setValue("branch", "");
}}
>
{repo.name}
<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"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{status === "loading" && 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 === "loading" && 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);
}}
>
{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>
)}
/>
</div>
<div className="flex w-full justify-end">
<Button
isLoading={isSavingGitlabProvider}
type="submit"
className="w-fit"
>
Save
</Button>
</div>
</form>
</Form>
</div>
);
};
@@ -4,20 +4,31 @@ import { SaveGithubProvider } from "@/components/dashboard/application/general/g
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { api } from "@/utils/api";
import { GitBranch, LockIcon } from "lucide-react";
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { SaveDragNDrop } from "./save-drag-n-drop";
import {
BitbucketIcon,
DockerIcon,
GithubIcon,
GitIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { SaveGitlabProvider } from "./save-gitlab-provider";
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
type TabState = "github" | "docker" | "git" | "drop";
type TabState = "github" | "docker" | "git" | "drop" | "gitlab" | "bitbucket";
interface Props {
applicationId: string;
}
export const ShowProviderForm = ({ applicationId }: Props) => {
const { data: haveGithubConfigured } =
api.admin.haveGithubConfigured.useQuery();
const { data: githubProviders } = api.gitProvider.githubProviders.useQuery();
const { data: gitlabProviders } = api.gitProvider.gitlabProviders.useQuery();
const { data: bitbucketProviders } =
api.gitProvider.bitbucketProviders.useQuery();
const { data: application } = api.application.one.useQuery({ applicationId });
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
@@ -44,34 +55,55 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
setSab(e as TabState);
}}
>
<TabsList className="grid w-fit grid-cols-4 bg-transparent">
<TabsTrigger
value="github"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Github
</TabsTrigger>
<TabsTrigger
value="docker"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Docker
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Git
</TabsTrigger>
<TabsTrigger
value="drop"
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
Drop
</TabsTrigger>
</TabsList>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList className="md:grid md:w-fit md:grid-cols-7 max-md:overflow-x-scroll justify-start bg-transparent overflow-y-hidden">
<TabsTrigger
value="github"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GithubIcon className="size-4 text-current fill-current" />
Github
</TabsTrigger>
<TabsTrigger
value="gitlab"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitlabIcon className="size-4 text-current fill-current" />
Gitlab
</TabsTrigger>
<TabsTrigger
value="bitbucket"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<BitbucketIcon className="size-4 text-current fill-current" />
Bitbucket
</TabsTrigger>
<TabsTrigger
value="docker"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<DockerIcon className="size-5 text-current" />
Docker
</TabsTrigger>
<TabsTrigger
value="git"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<GitIcon />
Git
</TabsTrigger>
<TabsTrigger
value="drop"
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
>
<UploadCloud className="size-5 text-current" />
Drop
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="github" className="w-full p-2">
{haveGithubConfigured ? (
{githubProviders && githubProviders?.length > 0 ? (
<SaveGithubProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3">
@@ -80,7 +112,47 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
To deploy using GitHub, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/server"
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="gitlab" className="w-full p-2">
{gitlabProviders && gitlabProviders?.length > 0 ? (
<SaveGitlabProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<GitlabIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using GitLab, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
)}
</TabsContent>
<TabsContent value="bitbucket" className="w-full p-2">
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
<SaveBitbucketProvider applicationId={applicationId} />
) : (
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
<BitbucketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To deploy using Bitbucket, you need to configure your account
first. Please, go to{" "}
<Link
href="/dashboard/settings/git-providers"
className="text-foreground"
>
Settings
@@ -93,6 +165,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
<TabsContent value="docker" className="w-full p-2">
<SaveDockerProvider applicationId={applicationId} />
</TabsContent>
<TabsContent value="git" className="w-full p-2">
<SaveGitProvider applicationId={applicationId} />
</TabsContent>
@@ -0,0 +1,230 @@
import {
BitbucketIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
username: z.string().min(1, {
message: "Username is required",
}),
password: z.string().min(1, {
message: "App Password is required",
}),
workspaceName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
export const AddBitbucketProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { mutateAsync, error, isError } =
api.gitProvider.createBitbucketProvider.useMutation();
const { data: auth } = api.auth.get.useQuery();
const router = useRouter();
const form = useForm<Schema>({
defaultValues: {
username: "",
password: "",
workspaceName: "",
},
resolver: zodResolver(Schema),
});
useEffect(() => {
form.reset({
username: "",
password: "",
workspaceName: "",
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
bitbucketUsername: data.username,
appPassword: data.password,
bitbucketWorkspaceName: data.workspaceName || "",
authId: auth?.id || "",
name: data.name || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("Bitbucket configured successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error configuring Bitbucket");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="secondary"
className="flex items-center space-x-1 bg-blue-700 text-white hover:bg-blue-600"
>
<BitbucketIcon />
<span>Bitbucket</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Bitbucket Provider <BitbucketIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-bitbucket"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your Bitbucket account, you need to create a new
App Password in your Bitbucket settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Create new App Password{" "}
<Link
href="https://bitbucket.org/account/settings/app-passwords/new"
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>
When creating the App Password, ensure you grant the
following permissions:
<ul className="list-disc list-inside ml-4">
<li>Account: Read</li>
<li>Workspace membership: Read</li>
<li>Projects: Read</li>
<li>Repositories: Read</li>
<li>Pull requests: Read</li>
<li>Webhooks: Read and write</li>
</ul>
</li>
<li>
After creating, you'll receive an App Password. Copy it and
paste it below along with your Bitbucket username.
</li>
</ol>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Bitbucket Username</FormLabel>
<FormControl>
<Input
placeholder="Your Bitbucket username"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>App Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="workspaceName"
render={({ field }) => (
<FormItem>
<FormLabel>Workspace Name (Optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization accounts"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={form.formState.isSubmitting}>
Configure Bitbucket
</Button>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,128 @@
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { format } from "date-fns";
import { useEffect, useState } from "react";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data } = api.auth.get.useQuery();
const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false);
const [organizationName, setOrganization] = useState("");
useEffect(() => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/providers/github/redirect?authId=${data?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {
url: `${url}/api/deploy/github`,
},
callback_urls: [`${origin}/api/providers/github/redirect`],
public: false,
request_oauth_on_install: true,
default_permissions: {
contents: "read",
metadata: "read",
emails: "read",
pull_requests: "write",
},
default_events: ["pull_request", "push"],
},
null,
4,
);
setManifest(manifest);
}, [data?.id]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="secondary" className="flex items-center space-x-1">
<GithubIcon />
<span>Github</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl ">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Github Provider <GithubIcon className="size-5" />
</DialogTitle>
</DialogHeader>
<div id="hook-form-add-project" className="grid w-full gap-1">
<CardContent className="p-0">
<div className="flex flex-col ">
<p className="text-muted-foreground text-sm">
To integrate your GitHub account with our services, you'll need
to create and install a GitHub app. This process is
straightforward and only takes a few minutes. Click the button
below to get started.
</p>
<div className="mt-4 flex flex-col gap-4">
<div className="flex flex-row gap-4">
<span>Organization?</span>
<Switch
checked={isOrganization}
onCheckedChange={(checked) => setIsOrganization(checked)}
/>
</div>
{isOrganization && (
<Input
required
placeholder="Organization name"
onChange={(e) => setOrganization(e.target.value)}
/>
)}
</div>
<form
action={
isOrganization
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.id}`
: `https://github.com/settings/apps/new?state=gh_init:${data?.id}`
}
method="post"
>
<input
type="text"
name="manifest"
id="manifest"
defaultValue={manifest}
className="invisible"
/>
<br />
<div className="flex w-full justify-end">
<Button
disabled={isOrganization && organizationName.length < 1}
type="submit"
className="self-end"
>
Create GitHub App
</Button>
</div>
</form>
</div>
</CardContent>
</div>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,250 @@
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { toast } from "sonner";
const Schema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
applicationId: z.string().min(1, {
message: "Application ID is required",
}),
applicationSecret: z.string().min(1, {
message: "Application Secret is required",
}),
redirectUri: z.string().min(1, {
message: "Redirect URI is required",
}),
groupName: z.string().optional(),
});
type Schema = z.infer<typeof Schema>;
export const AddGitlabProvider = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const url = useUrl();
const { data: auth } = api.auth.get.useQuery();
const { mutateAsync, error, isError } =
api.gitProvider.createGitlabProvider.useMutation();
const webhookUrl = `${url}/api/providers/gitlab/callback`;
const form = useForm<Schema>({
defaultValues: {
applicationId: "",
applicationSecret: "",
groupName: "",
redirectUri: webhookUrl,
},
resolver: zodResolver(Schema),
});
useEffect(() => {
form.reset({
applicationId: "",
applicationSecret: "",
groupName: "",
redirectUri: webhookUrl,
});
}, [form, isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
applicationId: data.applicationId || "",
secret: data.applicationSecret || "",
groupName: data.groupName || "",
authId: auth?.id || "",
name: data.name || "",
redirectUri: data.redirectUri || "",
})
.then(async () => {
await utils.gitProvider.getAll.invalidate();
toast.success("GitLab created successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error configuring GitLab");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="flex items-center space-x-1 bg-purple-700 text-white hover:bg-purple-600"
>
<GitlabIcon />
<span>GitLab</span>
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen ">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
GitLab Provider <GitlabIcon className="size-5" />
</DialogTitle>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-add-gitlab"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-1"
>
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your GitLab account, you need to create a new
application in your GitLab settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2 items-center">
Go to your GitLab profile settings{" "}
<Link
href="https://gitlab.com/-/profile/applications"
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>Navigate to Applications</li>
<li>
Create a new application with the following details:
<ul className="list-disc list-inside ml-4">
<li>Name: Dokploy</li>
<li>
Redirect URI:{" "}
<span className="text-primary">{webhookUrl}</span>{" "}
</li>
<li>Scopes: api, read_user, read_repository</li>
</ul>
</li>
<li>
After creating, you'll receive an Application ID and Secret,
copy them and paste them below.
</li>
</ol>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="redirectUri"
render={({ field }) => (
<FormItem>
<FormLabel>Redirect URI</FormLabel>
<FormControl>
<Input
disabled
placeholder="Random Name eg(my-personal-account)"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="applicationId"
render={({ field }) => (
<FormItem>
<FormLabel>Application ID</FormLabel>
<FormControl>
<Input placeholder="Application ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="applicationSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Application Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Application Secret"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="groupName"
render={({ field }) => (
<FormItem>
<FormLabel>Group Name (Optional)</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button isLoading={form.formState.isSubmitting}>
Configure GitLab App
</Button>
</div>
</CardContent>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,144 @@
import { buttonVariants } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { AddGitlabProvider } from "./add-gitlab-provider";
import {
BitbucketIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { AddGithubProvider } from "./add-github-provider";
import { AddBitbucketProvider } from "./add-bitbucket-provider";
import { api } from "@/utils/api";
import Link from "next/link";
import { RemoveGitProvider } from "../github/remove-github-app";
import { useUrl } from "@/utils/hooks/use-url";
export const ShowGitProviders = () => {
const { data } = api.gitProvider.getAll.useQuery();
const url = useUrl();
const getGitlabUrl = (clientId: string, gitlabId: string) => {
const redirectUri = `${url}/api/providers/gitlab/callback?gitlabId=${gitlabId}`;
const scope = "api read_user read_repository";
const authUrl = `https://gitlab.com/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${encodeURIComponent(scope)}`;
return authUrl;
};
return (
<div className="p-6 space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-bold">Git Providers</h1>
<p className="text-muted-foreground">
Connect your Git Providers to use it for login.
</p>
</div>
<Card className=" bg-transparent">
<CardContent className="p-4">
<div className="flex gap-4 sm:flex-row flex-col w-full">
<AddGithubProvider />
<AddGitlabProvider />
<AddBitbucketProvider />
</div>
</CardContent>
</Card>
{data?.map((gitProvider, index) => {
const isGithub = gitProvider.providerType === "github";
const isGitlab = gitProvider.providerType === "gitlab";
const haveGithubRequirements =
gitProvider.providerType === "github" &&
gitProvider.githubProvider?.githubPrivateKey &&
gitProvider.githubProvider?.githubAppId &&
gitProvider.githubProvider?.githubInstallationId;
const haveGitlabRequirements =
gitProvider.gitlabProvider?.accessToken &&
gitProvider.gitlabProvider?.refreshToken;
return (
<div
className="space-y-4"
key={`${gitProvider.gitProviderId}-${index}`}
>
<Card className="flex sm:flex-row max-sm:gap-2 flex-col justify-between items-center p-4">
<div className="flex items-center space-x-4 w-full">
{gitProvider.providerType === "github" && (
<GithubIcon className="w-6 h-6" />
)}
{gitProvider.providerType === "gitlab" && (
<GitlabIcon className="w-6 h-6" />
)}
{gitProvider.providerType === "bitbucket" && (
<BitbucketIcon className="w-6 h-6" />
)}
<div>
<p className="font-medium">
{gitProvider.providerType === "github"
? "GitHub"
: gitProvider.providerType === "gitlab"
? "GitLab"
: "Bitbucket"}
</p>
<p className="text-sm text-muted-foreground">
{gitProvider.name}
</p>
</div>
</div>
<div className="flex sm:gap-4 sm:flex-row flex-col">
{!haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1">
<Link
href={`${gitProvider?.githubProvider?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.githubProvider.githubProviderId}`}
className={buttonVariants({ className: "w-fit" })}
>
Install Github App
</Link>
</div>
)}
{haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1">
<Link
href={`${gitProvider?.githubProvider?.githubAppName}`}
target="_blank"
className={buttonVariants({
className: "w-fit",
variant: "secondary",
})}
>
<span className="text-sm">Manage Github App</span>
</Link>
</div>
)}
{!haveGitlabRequirements && isGitlab && (
<div className="flex flex-col gap-1">
<Link
href={getGitlabUrl(
gitProvider.gitlabProvider?.applicationId || "",
gitProvider.gitlabProvider?.gitlabProviderId || "",
)}
target="_blank"
className={buttonVariants({
className: "w-fit",
variant: "secondary",
})}
>
<span className="text-sm">Install Gitlab App</span>
</Link>
</div>
)}
<RemoveGitProvider
gitProviderId={gitProvider.gitProviderId}
gitProviderType={gitProvider.providerType}
/>
</div>
</Card>
</div>
);
})}
</div>
);
};
@@ -1,166 +0,0 @@
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { format } from "date-fns";
import { BadgeCheck } from "lucide-react";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { RemoveGithubApp } from "./remove-github-app";
export const GithubSetup = () => {
const [isOrganization, setIsOrganization] = useState(false);
const { data: haveGithubConfigured } =
api.admin.haveGithubConfigured.useQuery();
const [manifest, setManifest] = useState<string>("");
const [organizationName, setOrganization] = useState<string>("");
const { data } = api.admin.one.useQuery();
useEffect(() => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {
url: `${url}/api/deploy/github`,
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
},
callback_urls: [`${origin}/api/redirect`], // Los URLs de callback para procesos de autenticación
public: false,
request_oauth_on_install: true,
default_permissions: {
contents: "read",
metadata: "read",
emails: "read",
pull_requests: "write",
},
default_events: ["pull_request", "push"],
},
null,
4,
);
setManifest(manifest);
}, [data?.authId]);
return (
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Configure Github </CardTitle>
<CardDescription>
Setup your github account to access to your repositories.
</CardDescription>
</CardHeader>
<CardContent className="h-full space-y-2">
{haveGithubConfigured ? (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-4">
<span className="text-muted-foreground text-sm">
Github account configured succesfully.
</span>
<BadgeCheck className="size-4 text-green-700" />
</div>
<div className="flex items-end gap-4 flex-wrap">
<RemoveGithubApp />
<Link
href={`${data?.githubAppName}`}
target="_blank"
className={buttonVariants({
className: "w-fit",
variant: "secondary",
})}
>
<span className="text-sm">Manage Github App</span>
</Link>
</div>
</div>
) : (
<>
{data?.githubAppName ? (
<div className="flex w-fit flex-col gap-4">
<span className="text-muted-foreground">
You've successfully created a github app named{" "}
<strong>{data.githubAppName}</strong>! The next step is to
install this app in your GitHub account.
</span>
<div className="flex flex-row gap-4">
<Link
href={`${
data.githubAppName
}/installations/new?state=gh_setup:${data?.authId}`}
className={buttonVariants({ className: "w-fit" })}
>
Install Github App
</Link>
<RemoveGithubApp />
</div>
</div>
) : (
<div>
<div className="flex items-center gap-2">
<p className="text-muted-foreground text-sm">
To integrate your GitHub account with our services, you'll
need to create and install a GitHub app. This process is
straightforward and only takes a few minutes. Click the
button below to get started.
</p>
</div>
<div className="mt-4 flex flex-col gap-4">
<div className="flex flex-row gap-4">
<span>Organization?</span>
<Switch
checked={isOrganization}
onCheckedChange={(checked) => setIsOrganization(checked)}
/>
</div>
{isOrganization && (
<Input
required
placeholder="Organization name"
onChange={(e) => setOrganization(e.target.value)}
/>
)}
</div>
<form
action={
isOrganization
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${data?.authId}`
: `https://github.com/settings/apps/new?state=gh_init:${data?.authId}`
}
method="post"
>
<input
type="text"
name="manifest"
id="manifest"
defaultValue={manifest}
className="invisible"
/>
<br />
<Button
disabled={isOrganization && organizationName.length < 1}
type="submit"
>
Create GitHub App
</Button>
</form>
</div>
)}
</>
)}
</CardContent>
</Card>
);
};
@@ -0,0 +1,135 @@
import React, { useState, useEffect } from "react";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { BadgeCheck, ExternalLink } from "lucide-react";
import Link from "next/link";
import { api } from "@/utils/api";
export const GitlabSetup = () => {
const [applicationId, setApplicationId] = useState("");
const [applicationSecret, setApplicationSecret] = useState("");
const haveGitlabConfigured = false;
const [url, setUrl] = useState("");
// const { data: haveGitlabConfigured } =
// api.admin.haveGitlabConfigured.useQuery();
const { data: adminData } = api.admin.one.useQuery();
useEffect(() => {
const protocolAndHost = `${window.location.protocol}//${window.location.host}`;
setUrl(`${protocolAndHost}`);
}, [adminData]);
// const createGitlabApp = api.admin.createGitlabApp.useMutation();
const handleCreateApp = async () => {
// try {
// // await createGitlabApp.mutateAsync({
// // applicationId,
// // applicationSecret,
// // callbackUrl: `${window.location.origin}/api/gitlab/callback`,
// // });
// // Refetch the configuration status
// // await haveGitlabConfigured.refetch();
// } catch (error) {
// console.error("Failed to create GitLab app", error);
// }
};
return (
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Configure GitLab</CardTitle>
<CardDescription>
Setup your GitLab account to access your repositories.
</CardDescription>
</CardHeader>
<CardContent className="h-full space-y-2">
{haveGitlabConfigured ? (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-4">
<span className="text-muted-foreground text-sm">
GitLab account configured successfully.
</span>
<BadgeCheck className="size-4 text-green-700" />
</div>
<div className="flex items-end gap-4 flex-wrap">
<Button
variant="destructive"
onClick={() => {
/* Implement remove GitLab app logic */
}}
>
Remove GitLab App
</Button>
<Link
href="https://gitlab.com/-/profile/applications"
target="_blank"
className={buttonVariants({
className: "w-fit",
variant: "secondary",
})}
>
<span className="text-sm">Manage GitLab App</span>
</Link>
</div>
</div>
) : (
<div className="flex flex-col gap-4">
<p className="text-muted-foreground text-sm">
To integrate your GitLab account, you need to create a new
application in your GitLab settings. Follow these steps:
</p>
<ol className="list-decimal list-inside text-sm text-muted-foreground">
<li className="flex flex-row gap-2">
Go to your GitLab profile settings{" "}
<Link
href="https://gitlab.com/-/profile/applications"
target="_blank"
>
<ExternalLink className="w-fit text-primary size-4" />
</Link>
</li>
<li>Navigate to Applications</li>
<li>
Create a new application with the following details:
<ul className="list-disc list-inside ml-4">
<li>Name: Dokploy</li>
<li>Redirect URI: {`${url}/api/gitlab/callback`}</li>
<li>Scopes: api, read_user, read_repository</li>
</ul>
</li>
<li>
After creating, you'll receive an Application ID and Secret
</li>
</ol>
<Input
placeholder="Application ID"
value={applicationId}
onChange={(e) => setApplicationId(e.target.value)}
/>
<Input
type="password"
placeholder="Application Secret"
value={applicationSecret}
onChange={(e) => setApplicationSecret(e.target.value)}
/>
<Button
onClick={handleCreateApp}
disabled={!applicationId || !applicationSecret}
>
Configure GitLab App
</Button>
</div>
)}
</CardContent>
</Card>
);
};
@@ -17,31 +17,40 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { InfoIcon } from "lucide-react";
import { InfoIcon, TrashIcon } from "lucide-react";
import React from "react";
import { toast } from "sonner";
export const RemoveGithubApp = () => {
const { refetch } = api.auth.get.useQuery();
interface Props {
gitProviderId: string;
gitProviderType: "github" | "gitlab" | "bitbucket";
}
export const RemoveGitProvider = ({
gitProviderId,
gitProviderType,
}: Props) => {
const utils = api.useUtils();
const { mutateAsync } = api.admin.cleanGithubApp.useMutation();
const { mutateAsync } = api.gitProvider.remove.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">
Remove Current Github App
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-4 fill-muted-destructive text-muted-destructive" />
</TooltipTrigger>
<TooltipContent>
We recommend deleting the GitHub app first, and then removing
the current one from here.
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button variant="ghost">
<TrashIcon className="size-4 text-muted-destructive" />
{gitProviderType === "github" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon className="size-4 fill-muted-destructive text-muted-destructive" />
</TooltipTrigger>
<TooltipContent>
We recommend deleting the GitHub app first, and then removing
the current one from here.
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
@@ -56,15 +65,15 @@ export const RemoveGithubApp = () => {
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync()
await mutateAsync({
gitProviderId: gitProviderId,
})
.then(async () => {
await refetch();
utils.admin.one.invalidate();
await utils.admin.haveGithubConfigured.invalidate();
toast.success("Github application deleted succesfully.");
utils.gitProvider.getAll.invalidate();
toast.success("Git Provider deleted succesfully.");
})
.catch(() => {
toast.error("Error to delete your github application.");
toast.error("Error to delete your git provider.");
});
}}
>
@@ -1,40 +0,0 @@
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import React from "react";
import { AppearanceForm } from "./appearance-form";
import { ShowCertificates } from "./certificates/show-certificates";
import { ShowDestinations } from "./destination/show-destinations";
import { GithubSetup } from "./github/github-setup";
import { ProfileForm } from "./profile/profile-form";
import { ShowUsers } from "./users/show-users";
import { WebDomain } from "./web-domain";
import { WebServer } from "./web-server";
export const ShowSettings = () => {
const { data } = api.auth.get.useQuery();
return (
<div
className={cn(
"mt-6 md:grid flex flex-col gap-4 pb-20 md:grid-cols-2",
data?.rol === "user" && "col-span-2",
)}
>
<div className={cn(data?.rol === "user" && "col-span-2")}>
<ProfileForm />
</div>
{data?.rol === "admin" && (
<>
<GithubSetup />
<AppearanceForm />
<ShowDestinations />
<ShowCertificates />
<WebDomain />
<WebServer />
<ShowUsers />
</>
)}
</div>
);
};
@@ -1,3 +1,4 @@
import { cn } from "@/lib/utils";
import React from "react";
// https://worldvectorlogo.com/downloaded/redis Ref
@@ -155,3 +156,116 @@ export const RedisIcon = ({ className }: Props) => {
</svg>
);
};
export const GitlabIcon = ({ className }: Props) => {
return (
<svg
aria-label="gitlab"
height="14"
viewBox="0 0 24 22"
width="14"
className={cn("fill-white text-white", className)}
>
<path
d="M1.279 8.29L.044 12.294c-.117.367 0 .78.325 1.014l11.323 8.23-.009-.012-.03-.039L1.279 8.29zM22.992 13.308a.905.905 0 00.325-1.014L22.085 8.29 11.693 21.52l11.299-8.212z"
fill="currentColor"
/>
<path
d="M1.279 8.29l10.374 13.197.03.039.01-.006L22.085 8.29H1.28z"
fill="currentColor"
opacity="0.4"
/>
<path
d="M15.982 8.29l-4.299 13.236-.004.011.014-.017L22.085 8.29h-6.103zM7.376 8.29H1.279l10.374 13.197L7.376 8.29z"
fill="currentColor"
opacity="0.6"
/>
<path
d="M18.582.308l-2.6 7.982h6.103L19.48.308c-.133-.41-.764-.41-.897 0zM1.279 8.29L3.88.308c.133-.41.764-.41.897 0l2.6 7.982H1.279z"
fill="currentColor"
opacity="0.4"
/>
</svg>
);
};
export const GithubIcon = ({ className }: Props) => {
return (
<svg
aria-label="github"
height="18"
viewBox="0 0 14 14"
width="18"
className={className}
>
<path
d="M7 .175c-3.872 0-7 3.128-7 7 0 3.084 2.013 5.71 4.79 6.65.35.066.482-.153.482-.328v-1.181c-1.947.415-2.363-.941-2.363-.941-.328-.81-.787-1.028-.787-1.028-.634-.438.044-.416.044-.416.7.044 1.071.722 1.071.722.635 1.072 1.641.766 2.035.59.066-.459.24-.765.437-.94-1.553-.175-3.193-.787-3.193-3.456 0-.766.262-1.378.721-1.881-.065-.175-.306-.897.066-1.86 0 0 .59-.197 1.925.722a6.754 6.754 0 0 1 1.75-.24c.59 0 1.203.087 1.75.24 1.335-.897 1.925-.722 1.925-.722.372.963.131 1.685.066 1.86.46.48.722 1.115.722 1.88 0 2.691-1.641 3.282-3.194 3.457.24.219.481.634.481 1.29v1.926c0 .197.131.415.481.328C11.988 12.884 14 10.259 14 7.175c0-3.872-3.128-7-7-7z"
fill="#fff"
fillRule="nonzero"
/>
</svg>
);
};
export const BitbucketIcon = ({ className }: Props) => {
return (
<svg height="14" viewBox="-2 -2 65 59" width="14" className={className}>
<defs>
<linearGradient
id="bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:"
x1="104.953%"
x2="46.569%"
y1="21.921%"
y2="75.234%"
>
<stop offset="7%" stopColor="currentColor" stop-opacity=".4" />
<stop offset="100%" stopColor="currentColor" />
</linearGradient>
</defs>
<path
d="M59.696 18.86h-18.77l-3.15 18.39h-13L9.426 55.47a2.71 2.71 0 001.75.66h40.74a2 2 0 002-1.68l5.78-35.59z"
fill="url(#bitbucket-:R7aq37rqjt7rrrmpjtuj7l9qjtsr:)"
fillRule="nonzero"
transform="translate(-.026 .82)"
/>
<path
d="M2 .82a2 2 0 00-2 2.32l8.49 51.54a2.7 2.7 0 00.91 1.61 2.71 2.71 0 001.75.66l15.76-18.88H24.7l-3.47-18.39h38.44l2.7-16.53a2 2 0 00-2-2.32L2 .82z"
fill="currentColor"
fillRule="nonzero"
/>
</svg>
);
};
export const DockerIcon = ({ className }: Props) => {
return (
<svg
height="24"
viewBox="-.557 117.607 598.543 423.631"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="#0091e2">
<path d="m592.162 277.804c-1.664-1.37-16.642-12.597-48.815-12.597-8.321 0-16.92.822-25.24 2.191-6.102-41.898-41.327-62.162-42.714-63.257l-8.598-4.93-5.547 7.942c-6.934 10.68-12.204 22.729-15.255 35.052-5.824 23.824-2.219 46.279 9.985 65.447-14.7 8.216-38.553 10.133-43.545 10.406h-393.853c-10.262 0-18.583 8.216-18.583 18.348-.554 33.956 5.27 67.912 17.197 99.951 13.59 35.052 33.838 61.067 59.91 76.95 29.4 17.799 77.383 27.931 131.468 27.931 24.408 0 48.815-2.19 72.946-6.572 33.56-6.025 65.734-17.526 95.412-34.23a260.485 260.485 0 0 0 64.902-52.577c31.342-34.778 49.925-73.663 63.515-108.167h5.547c34.116 0 55.195-13.418 66.844-24.92 7.766-7.12 13.59-15.882 17.751-25.74l2.497-7.12z" />
<path d="m55.193 306.83h52.698c2.497 0 4.716-1.916 4.716-4.654v-46.553c0-2.465-1.942-4.655-4.716-4.655h-52.698c-2.496 0-4.715 1.916-4.715 4.655v46.553c.277 2.738 2.219 4.655 4.715 4.655zm72.668 0h52.699c2.496 0 4.715-1.916 4.715-4.654v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.7c-2.496 0-4.715 1.916-4.715 4.655v46.553c.278 2.738 2.22 4.655 4.715 4.655m74.055 0h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.699c-2.496 0-4.715 1.916-4.715 4.655v46.553c0 2.738 1.942 4.655 4.715 4.655zm72.946 0h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.942-4.655-4.715-4.655h-52.699c-2.496 0-4.715 1.916-4.715 4.655v46.553c0 2.738 2.219 4.655 4.715 4.655zm-147-66.543h52.698c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-1.942-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c.278 2.464 2.22 4.655 4.715 4.655m74.055 0h52.699c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-1.942-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 1.942 4.655 4.715 4.655m72.946 0h52.699c2.496 0 4.715-2.19 4.715-4.655v-46.553c0-2.465-2.22-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 2.219 4.655 4.715 4.655m0-66.817h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-2.22-4.656-4.715-4.656h-52.699c-2.496 0-4.715 1.917-4.715 4.656v46.553c0 2.464 2.219 4.655 4.715 4.655m73.5 133.36h52.699c2.496 0 4.715-1.917 4.715-4.655v-46.553c0-2.465-1.941-4.655-4.715-4.655h-52.698c-2.497 0-4.716 1.916-4.716 4.655v46.553c.278 2.738 2.22 4.655 4.716 4.655" />
</g>
</svg>
);
};
export const GitIcon = ({ className }: Props) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMinYMin meet"
>
<path
d="M251.172 116.594L139.4 4.828c-6.433-6.437-16.873-6.437-23.314 0l-23.21 23.21 29.443 29.443c6.842-2.312 14.688-.761 20.142 4.693 5.48 5.489 7.02 13.402 4.652 20.266l28.375 28.376c6.865-2.365 14.786-.835 20.269 4.657 7.663 7.66 7.663 20.075 0 27.74-7.665 7.666-20.08 7.666-27.749 0-5.764-5.77-7.188-14.235-4.27-21.336l-26.462-26.462-.003 69.637a19.82 19.82 0 0 1 5.188 3.71c7.663 7.66 7.663 20.076 0 27.747-7.665 7.662-20.086 7.662-27.74 0-7.663-7.671-7.663-20.086 0-27.746a19.654 19.654 0 0 1 6.421-4.281V94.196a19.378 19.378 0 0 1-6.421-4.281c-5.806-5.798-7.202-14.317-4.227-21.446L81.47 39.442l-76.64 76.635c-6.44 6.443-6.44 16.884 0 23.322l111.774 111.768c6.435 6.438 16.873 6.438 23.316 0l111.251-111.249c6.438-6.44 6.438-16.887 0-23.324"
fill="#DE4C36"
/>
</svg>
);
};
@@ -59,6 +59,12 @@ export const SettingsLayout = ({ children }: Props) => {
icon: KeyRound,
href: "/dashboard/settings/ssh-keys",
},
{
title: "Git ",
label: "",
icon: GitBranch,
href: "/dashboard/settings/git-providers",
},
{
title: "Users",
label: "",
@@ -102,6 +108,7 @@ import {
Activity,
Bell,
Database,
GitBranch,
KeyRound,
type LucideIcon,
Route,