mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-25 22:42:18 +00:00
Merge branch 'canary' into feat/docker-build-stage
This commit is contained in:
+378
@@ -0,0 +1,378 @@
|
||||
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"),
|
||||
bitbucketId: 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.bitbucket.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: "",
|
||||
},
|
||||
bitbucketId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(BitbucketProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const bitbucketId = form.watch("bitbucketId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
isError,
|
||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||
{
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.bitbucket.getBitbucketBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.bitbucketBranch || "",
|
||||
repository: {
|
||||
repo: data.bitbucketRepository || "",
|
||||
owner: data.bitbucketOwner || "",
|
||||
},
|
||||
buildPath: data.bitbucketBuildPath || "/",
|
||||
bitbucketId: data.bitbucketId || "",
|
||||
});
|
||||
}
|
||||
}, [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,
|
||||
bitbucketId: data.bitbucketId,
|
||||
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="bitbucketId"
|
||||
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.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">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
+65
-3
@@ -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"),
|
||||
githubId: 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.github.githubProviders.useQuery();
|
||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGithubProvider } =
|
||||
@@ -60,26 +69,38 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
githubId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GithubProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const githubId = form.watch("githubId");
|
||||
|
||||
const { data: repositories, isLoading: isLoadingRepositories } =
|
||||
api.admin.getRepositories.useQuery();
|
||||
api.github.getGithubRepositories.useQuery(
|
||||
{
|
||||
githubId,
|
||||
},
|
||||
{
|
||||
enabled: !!githubId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.admin.getBranches.useQuery(
|
||||
} = api.github.getGithubBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
githubId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!githubId,
|
||||
},
|
||||
{ enabled: !!repository?.owner && !!repository?.repo },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -91,6 +112,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
owner: data.owner || "",
|
||||
},
|
||||
buildPath: data.buildPath || "/",
|
||||
githubId: data.githubId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
@@ -102,6 +124,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
applicationId,
|
||||
owner: data.repository.owner,
|
||||
buildPath: data.buildPath,
|
||||
githubId: data.githubId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
@@ -120,6 +143,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="githubId"
|
||||
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.githubId}
|
||||
value={githubProvider.githubId}
|
||||
>
|
||||
{githubProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
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"),
|
||||
gitlabPathNamespace: z.string().min(1),
|
||||
id: z.number().nullable(),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
gitlabId: 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.gitlab.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: "",
|
||||
gitlabPathNamespace: "",
|
||||
id: null,
|
||||
},
|
||||
gitlabId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GitlabProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const gitlabId = form.watch("gitlabId");
|
||||
|
||||
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 || "",
|
||||
});
|
||||
}
|
||||
}, [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,
|
||||
gitlabId: data.gitlabId,
|
||||
applicationId,
|
||||
gitlabProjectId: data.repository.id,
|
||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||
})
|
||||
.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="gitlabId"
|
||||
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: "",
|
||||
id: null,
|
||||
gitlabPathNamespace: "",
|
||||
});
|
||||
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.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">
|
||||
<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 && 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", "");
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,34 @@
|
||||
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
|
||||
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
|
||||
import { SaveGithubProvider } from "@/components/dashboard/application/general/generic/save-github-provider";
|
||||
import {
|
||||
BitbucketIcon,
|
||||
DockerIcon,
|
||||
GitIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
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 { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-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.github.githubProviders.useQuery();
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
@@ -44,43 +55,104 @@ 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">
|
||||
<LockIcon className="size-8 text-muted-foreground" />
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GithubIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
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>
|
||||
|
||||
@@ -166,7 +166,13 @@ export const AddDomainCompose = ({
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="flex flex-col gap-4">
|
||||
<AlertBlock type="info">
|
||||
Deploy is required to apply changes after creating or updating a
|
||||
domain.
|
||||
</AlertBlock>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
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({
|
||||
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"),
|
||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||
});
|
||||
|
||||
type BitbucketProvider = z.infer<typeof BitbucketProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingBitbucketProvider } =
|
||||
api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<BitbucketProvider>({
|
||||
defaultValues: {
|
||||
composePath: "./docker-compose.yml",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
bitbucketId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(BitbucketProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const bitbucketId = form.watch("bitbucketId");
|
||||
|
||||
const {
|
||||
data: repositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
error,
|
||||
isError,
|
||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||
{
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.bitbucket.getBitbucketBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId,
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
branch: data.bitbucketBranch || "",
|
||||
repository: {
|
||||
repo: data.bitbucketRepository || "",
|
||||
owner: data.bitbucketOwner || "",
|
||||
},
|
||||
composePath: data.composePath,
|
||||
bitbucketId: data.bitbucketId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: BitbucketProvider) => {
|
||||
await mutateAsync({
|
||||
bitbucketBranch: data.branch,
|
||||
bitbucketRepository: data.repository.repo,
|
||||
bitbucketOwner: data.repository.owner,
|
||||
bitbucketId: data.bitbucketId,
|
||||
composePath: data.composePath,
|
||||
composeId,
|
||||
sourceType: "bitbucket",
|
||||
composeStatus: "idle",
|
||||
})
|
||||
.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="bitbucketId"
|
||||
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.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">
|
||||
<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="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...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>
|
||||
);
|
||||
};
|
||||
+68
-7
@@ -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"),
|
||||
githubId: z.string().min(1, "Github Provider is required"),
|
||||
});
|
||||
|
||||
type GithubProvider = z.infer<typeof GithubProviderSchema>;
|
||||
@@ -48,6 +56,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGithubProvider } =
|
||||
@@ -60,26 +69,38 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
owner: "",
|
||||
repo: "",
|
||||
},
|
||||
githubId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GithubProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const githubId = form.watch("githubId");
|
||||
|
||||
const { data: repositories, isLoading: isLoadingRepositories } =
|
||||
api.admin.getRepositories.useQuery();
|
||||
api.github.getGithubRepositories.useQuery(
|
||||
{
|
||||
githubId,
|
||||
},
|
||||
{
|
||||
enabled: !!githubId,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
fetchStatus,
|
||||
status,
|
||||
} = api.admin.getBranches.useQuery(
|
||||
} = api.github.getGithubBranches.useQuery(
|
||||
{
|
||||
owner: repository?.owner,
|
||||
repo: repository?.repo,
|
||||
githubId,
|
||||
},
|
||||
{
|
||||
enabled: !!repository?.owner && !!repository?.repo && !!githubId,
|
||||
},
|
||||
{ enabled: !!repository?.owner && !!repository?.repo },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -91,19 +112,21 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
owner: data.owner || "",
|
||||
},
|
||||
composePath: data.composePath,
|
||||
githubId: data.githubId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GithubProvider) => {
|
||||
console.log(data);
|
||||
await mutateAsync({
|
||||
branch: data.branch,
|
||||
repository: data.repository.repo,
|
||||
composeId: composeId,
|
||||
composeId,
|
||||
owner: data.repository.owner,
|
||||
sourceType: "github",
|
||||
composePath: data.composePath,
|
||||
githubId: data.githubId,
|
||||
sourceType: "github",
|
||||
composeStatus: "idle",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Service Provided Saved");
|
||||
@@ -122,6 +145,45 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
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) => {
|
||||
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.githubId}
|
||||
value={githubProvider.githubId}
|
||||
>
|
||||
{githubProvider.gitProvider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repository"
|
||||
@@ -278,7 +340,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="composePath"
|
||||
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
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({
|
||||
composePath: z.string().min(1),
|
||||
repository: z
|
||||
.object({
|
||||
repo: z.string().min(1, "Repo is required"),
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
id: z.number().nullable(),
|
||||
gitlabPathNamespace: z.string().min(1),
|
||||
})
|
||||
.required(),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||
});
|
||||
|
||||
type GitlabProvider = z.infer<typeof GitlabProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { mutateAsync, isLoading: isSavingGitlabProvider } =
|
||||
api.compose.update.useMutation();
|
||||
|
||||
const form = useForm<GitlabProvider>({
|
||||
defaultValues: {
|
||||
composePath: "./docker-compose.yml",
|
||||
repository: {
|
||||
owner: "",
|
||||
repo: "",
|
||||
gitlabPathNamespace: "",
|
||||
id: null,
|
||||
},
|
||||
gitlabId: "",
|
||||
branch: "",
|
||||
},
|
||||
resolver: zodResolver(GitlabProviderSchema),
|
||||
});
|
||||
|
||||
const repository = form.watch("repository");
|
||||
const gitlabId = form.watch("gitlabId");
|
||||
|
||||
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 || "",
|
||||
id: data.gitlabProjectId,
|
||||
gitlabPathNamespace: data.gitlabPathNamespace || "",
|
||||
},
|
||||
composePath: data.composePath,
|
||||
gitlabId: data.gitlabId || "",
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (data: GitlabProvider) => {
|
||||
await mutateAsync({
|
||||
gitlabBranch: data.branch,
|
||||
gitlabRepository: data.repository.repo,
|
||||
gitlabOwner: data.repository.owner,
|
||||
composePath: data.composePath,
|
||||
gitlabId: data.gitlabId,
|
||||
composeId,
|
||||
gitlabProjectId: data.repository.id,
|
||||
gitlabPathNamespace: data.repository.gitlabPathNamespace,
|
||||
sourceType: "gitlab",
|
||||
composeStatus: "idle",
|
||||
})
|
||||
.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="gitlabId"
|
||||
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: "",
|
||||
gitlabPathNamespace: "",
|
||||
id: null,
|
||||
});
|
||||
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.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">
|
||||
<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 && 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", "");
|
||||
}}
|
||||
>
|
||||
{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="composePath"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Compose Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="docker-compose.yml" {...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>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,32 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GitIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
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 { CodeIcon, GitBranch, LockIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ComposeFileEditor } from "../compose-file-editor";
|
||||
import { ShowConvertedCompose } from "../show-converted-compose";
|
||||
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
|
||||
import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
||||
|
||||
type TabState = "github" | "git" | "raw";
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket";
|
||||
interface Props {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
const { data: haveGithubConfigured } =
|
||||
api.admin.haveGithubConfigured.useQuery();
|
||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||
const { data: bitbucketProviders } =
|
||||
api.bitbucket.bitbucketProviders.useQuery();
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||
@@ -44,38 +54,97 @@ export const ShowProviderFormCompose = ({ composeId }: 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>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 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="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="raw"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Raw
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<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="raw"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
<CodeIcon className="size-4 " />
|
||||
Raw
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="github" className="w-full p-2">
|
||||
{haveGithubConfigured ? (
|
||||
{githubProviders && githubProviders?.length > 0 ? (
|
||||
<SaveGithubProviderCompose composeId={composeId} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<LockIcon className="size-8 text-muted-foreground" />
|
||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||
<GithubIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
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 ? (
|
||||
<SaveGitlabProviderCompose composeId={composeId} />
|
||||
) : (
|
||||
<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 ? (
|
||||
<SaveBitbucketProviderCompose composeId={composeId} />
|
||||
) : (
|
||||
<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
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
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.bitbucket.create.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,201 @@
|
||||
import { BitbucketIcon } 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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Edit } from "lucide-react";
|
||||
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",
|
||||
}),
|
||||
workspaceName: z.string().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
interface Props {
|
||||
bitbucketId: string;
|
||||
}
|
||||
|
||||
export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
||||
const { data: bitbucket } = api.bitbucket.one.useQuery(
|
||||
{
|
||||
bitbucketId,
|
||||
},
|
||||
{
|
||||
enabled: !!bitbucketId,
|
||||
},
|
||||
);
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } = api.bitbucket.update.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading } =
|
||||
api.bitbucket.testConnection.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
workspaceName: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
const username = form.watch("username");
|
||||
const workspaceName = form.watch("workspaceName");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
username: bitbucket?.bitbucketUsername || "",
|
||||
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
|
||||
name: bitbucket?.gitProvider.name || "",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
bitbucketId,
|
||||
gitProviderId: bitbucket?.gitProviderId || "",
|
||||
bitbucketUsername: data.username,
|
||||
bitbucketWorkspaceName: data.workspaceName || "",
|
||||
name: data.name || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Bitbucket updated successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update Bitbucket");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Edit className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Update 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">
|
||||
<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="workspaceName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Workspace Name (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="For organization accounts"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex w-full justify-end gap-4 mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
bitbucketId,
|
||||
bitbucketUsername: username,
|
||||
workspaceName: workspaceName,
|
||||
})
|
||||
.then(async (message) => {
|
||||
toast.info(`Message: ${message}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
<Button type="submit" isLoading={form.formState.isSubmitting}>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</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/setup?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/setup`],
|
||||
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,154 @@
|
||||
import { GithubIcon } 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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Edit } from "lucide-react";
|
||||
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",
|
||||
}),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
interface Props {
|
||||
githubId: string;
|
||||
}
|
||||
|
||||
export const EditGithubProvider = ({ githubId }: Props) => {
|
||||
const { data: github } = api.github.one.useQuery(
|
||||
{
|
||||
githubId,
|
||||
},
|
||||
{
|
||||
enabled: !!githubId,
|
||||
},
|
||||
);
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } = api.github.update.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading } =
|
||||
api.github.testConnection.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
name: github?.gitProvider.name || "",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
githubId,
|
||||
name: data.name || "",
|
||||
gitProviderId: github?.gitProviderId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Github updated successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update Github");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Edit className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Update Github Provider <GithubIcon className="size-5" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-github"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-1"
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex w-full justify-end gap-4 mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
githubId,
|
||||
})
|
||||
.then(async (message) => {
|
||||
toast.info(`Message: ${message}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
<Button type="submit" isLoading={form.formState.isSubmitting}>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
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 { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
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.gitlab.create.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,180 @@
|
||||
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 { Edit } from "lucide-react";
|
||||
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",
|
||||
}),
|
||||
groupName: z.string().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
|
||||
interface Props {
|
||||
gitlabId: string;
|
||||
}
|
||||
|
||||
export const EditGitlabProvider = ({ gitlabId }: Props) => {
|
||||
const { data: gitlab } = api.gitlab.one.useQuery(
|
||||
{
|
||||
gitlabId,
|
||||
},
|
||||
{
|
||||
enabled: !!gitlabId,
|
||||
},
|
||||
);
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { mutateAsync, error, isError } = api.gitlab.update.useMutation();
|
||||
const { mutateAsync: testConnection, isLoading } =
|
||||
api.gitlab.testConnection.useMutation();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
groupName: "",
|
||||
name: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
const groupName = form.watch("groupName");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
groupName: gitlab?.groupName || "",
|
||||
name: gitlab?.gitProvider.name || "",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
gitlabId,
|
||||
gitProviderId: gitlab?.gitProviderId || "",
|
||||
groupName: data.groupName || "",
|
||||
name: data.name || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
toast.success("Gitlab updated successfully");
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update Gitlab");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Edit className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl overflow-y-auto max-h-screen">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Update GitLab Provider <GitlabIcon 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">
|
||||
<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="groupName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Group Name (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="For organization/group access"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex w-full justify-end gap-4 mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"secondary"}
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
gitlabId,
|
||||
groupName: groupName || "",
|
||||
})
|
||||
.then(async (message) => {
|
||||
toast.info(`Message: ${message}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
<Button type="submit" isLoading={form.formState.isSubmitting}>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+32
-23
@@ -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.");
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GitIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
import { formatDate } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||
import { EditBitbucketProvider } from "./bitbucket/edit-bitbucket-provider";
|
||||
import { AddGithubProvider } from "./github/add-github-provider";
|
||||
import { EditGithubProvider } from "./github/edit-github-provider";
|
||||
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
|
||||
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
|
||||
import { RemoveGitProvider } from "./remove-git-provider";
|
||||
|
||||
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 provider for authentication.
|
||||
</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>
|
||||
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-1">
|
||||
{data && data.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<GitIcon className="size-8" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
No Git Providers found. To add a provider, create a new one such
|
||||
as GitHub, GitLab, or Bitbucket.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{data?.map((gitProvider, index) => {
|
||||
const isGithub = gitProvider.providerType === "github";
|
||||
const isGitlab = gitProvider.providerType === "gitlab";
|
||||
const isBitbucket = gitProvider.providerType === "bitbucket";
|
||||
const haveGithubRequirements =
|
||||
gitProvider.providerType === "github" &&
|
||||
gitProvider.github?.githubPrivateKey &&
|
||||
gitProvider.github?.githubAppId &&
|
||||
gitProvider.github?.githubInstallationId;
|
||||
|
||||
const haveGitlabRequirements =
|
||||
gitProvider.gitlab?.accessToken && gitProvider.gitlab?.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 h-full bg-transparent">
|
||||
<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 className="flex flex-col gap-1">
|
||||
<p className="font-medium">
|
||||
{gitProvider.providerType === "github"
|
||||
? "GitHub"
|
||||
: gitProvider.providerType === "gitlab"
|
||||
? "GitLab"
|
||||
: "Bitbucket"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{gitProvider.name}
|
||||
</p>
|
||||
<span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Created{" "}
|
||||
{formatDate(
|
||||
gitProvider.createdAt,
|
||||
"yyyy-MM-dd hh:mm:ss a",
|
||||
)}
|
||||
</p>
|
||||
</span>
|
||||
</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?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
|
||||
className={buttonVariants({ className: "w-fit" })}
|
||||
>
|
||||
Install
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={`${gitProvider?.github?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Manage</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!haveGitlabRequirements && isGitlab && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Link
|
||||
href={getGitlabUrl(
|
||||
gitProvider.gitlab?.applicationId || "",
|
||||
gitProvider.gitlab?.gitlabId || "",
|
||||
)}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
variant: "secondary",
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Install</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row gap-1">
|
||||
{isBitbucket && (
|
||||
<EditBitbucketProvider
|
||||
bitbucketId={gitProvider.bitbucket.bitbucketId}
|
||||
/>
|
||||
)}
|
||||
{isGitlab && haveGitlabRequirements && (
|
||||
<EditGitlabProvider
|
||||
gitlabId={gitProvider.gitlab.gitlabId}
|
||||
/>
|
||||
)}
|
||||
{isGithub && haveGithubRequirements && (
|
||||
<EditGithubProvider
|
||||
githubId={gitProvider.github.githubId}
|
||||
/>
|
||||
)}
|
||||
<RemoveGitProvider
|
||||
gitProviderId={gitProvider.gitProviderId}
|
||||
gitProviderType={gitProvider.providerType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -40,6 +40,7 @@ const addPermissions = z.object({
|
||||
canAccessToDocker: z.boolean().optional().default(false),
|
||||
canAccessToAPI: z.boolean().optional().default(false),
|
||||
canAccessToSSHKeys: z.boolean().optional().default(false),
|
||||
canAccessToGitProviders: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
type AddPermissions = z.infer<typeof addPermissions>;
|
||||
@@ -84,6 +85,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||
});
|
||||
}
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||
@@ -101,6 +103,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Permissions updated");
|
||||
@@ -293,6 +296,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToGitProviders"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to Git Providers</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the Git Providers section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accesedProjects"
|
||||
|
||||
@@ -60,7 +60,9 @@ export const EditTraefikEnv = ({ children }: Props) => {
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync(data.env)
|
||||
await mutateAsync({
|
||||
env: data.env,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik Env Updated");
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import React from "react";
|
||||
|
||||
// https://worldvectorlogo.com/downloaded/redis Ref
|
||||
@@ -155,3 +156,118 @@ 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" stopOpacity=".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"
|
||||
className={className}
|
||||
>
|
||||
<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="20"
|
||||
height="20"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMinYMin meet"
|
||||
className={className}
|
||||
>
|
||||
<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: "",
|
||||
@@ -89,6 +95,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(user?.canAccessToGitProviders
|
||||
? [
|
||||
{
|
||||
title: "Git",
|
||||
label: "",
|
||||
icon: GitBranch,
|
||||
href: "/dashboard/settings/git-providers",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -102,6 +118,7 @@ import {
|
||||
Activity,
|
||||
Bell,
|
||||
Database,
|
||||
GitBranch,
|
||||
KeyRound,
|
||||
type LucideIcon,
|
||||
Route,
|
||||
|
||||
Reference in New Issue
Block a user