Update github integrations ui for organization and environment

This commit is contained in:
Salman
2024-02-15 09:41:57 +05:30
parent 9282dd08d9
commit c8f0796952
6 changed files with 311 additions and 143 deletions
@@ -358,20 +358,48 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
} }
}, },
handler: async (req) => { handler: async (req) => {
try { const orgs = await server.services.integrationAuth.getGithubOrgs({
const orgs = await server.services.integrationAuth.getGithubOrgs({ actorId: req.permission.id,
actorId: req.permission.id, actor: req.permission.type,
actor: req.permission.type, actorOrgId: req.permission.orgId,
actorOrgId: req.permission.orgId, id: req.params.integrationAuthId
id: req.params.integrationAuthId });
}); if (!orgs) throw new Error("No organization found.");
if (!orgs) throw new Error("No organization found.");
return { orgs: orgs || [] }; return { orgs };
} catch (e) { }
console.error(e); });
return { orgs: [] };
server.route({
url: "/:integrationAuthId/github/envs",
method: "GET",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
querystring: z.object({
repoOwner: z.string().trim(),
repoName: z.string().trim()
}),
response: {
200: z.object({
envs: z.object({ name: z.string(), envId: z.string() }).array()
})
} }
},
handler: async (req) => {
const envs = await server.services.integrationAuth.getGithubEnvs({
actorId: req.permission.id,
actor: req.permission.type,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
repoName: req.query.repoName,
repoOwner: req.query.repoOwner
});
if (!envs) throw new Error("No organization found.");
return { envs };
} }
}); });
@@ -24,6 +24,7 @@ import {
TIntegrationAuthAppsDTO, TIntegrationAuthAppsDTO,
TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO, TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthNorthflankSecretGroupDTO, TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryEnvironmentsDTO,
@@ -401,13 +402,51 @@ export const integrationAuthServiceFactory = ({
auth: accessToken auth: accessToken
}); });
const { data } = await octokit.request("GET /organizations", { const { data } = await octokit.request("GET /user/orgs", {
headers: { headers: {
"X-GitHub-Api-Version": "2022-11-28" "X-GitHub-Api-Version": "2022-11-28"
} }
}); });
if (!data) return []; if (!data) return [];
return data.map(({ login: name, id: orgId }) => ({ name, orgId })); return data.map(({ login: name, id: orgId }) => ({ name, orgId: String(orgId) }));
};
const getGithubEnvs = async ({
actorId,
actor,
actorOrgId,
id,
repoOwner,
repoName
}: TIntegrationAuthGithubEnvsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
const octokit = new Octokit({
auth: accessToken
});
const {
data: { environments }
} = await octokit.request("GET /repos/{owner}/{repo}/environments", {
headers: {
"X-GitHub-Api-Version": "2022-11-28"
},
owner: repoOwner,
repo: repoName
});
if (!environments) return [];
return environments.map(({ id: envId, name }) => ({ name, envId: String(envId) }));
}; };
const getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => { const getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => {
@@ -762,9 +801,7 @@ export const integrationAuthServiceFactory = ({
while (hasNextPage) { while (hasNextPage) {
// eslint-disable-next-line // eslint-disable-next-line
const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get( const { data }: { data: { values: TBitbucketWorkspace[]; next: string } } = await request.get(workspaceUrl, {
workspaceUrl,
{
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json" "Accept-Encoding": "application/json"
@@ -941,6 +978,7 @@ export const integrationAuthServiceFactory = ({
getVercelBranches, getVercelBranches,
getApps, getApps,
getGithubOrgs, getGithubOrgs,
getGithubEnvs,
getChecklyGroups, getChecklyGroups,
getQoveryApps, getQoveryApps,
getQoveryEnvs, getQoveryEnvs,
@@ -48,6 +48,12 @@ export type TIntegrationAuthGithubOrgsDTO = {
id: string; id: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthGithubEnvsDTO = {
id: string;
repoName: string;
repoOwner: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryOrgsDTO = { export type TIntegrationAuthQoveryOrgsDTO = {
id: string; id: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -6,6 +6,7 @@ export {
useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetIntegrationAuthChecklyGroups, useGetIntegrationAuthChecklyGroups,
useGetIntegrationAuthGithubEnvs,
useGetIntegrationAuthGithubOrgs, useGetIntegrationAuthGithubOrgs,
useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthNorthflankSecretGroups,
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayEnvironments,
@@ -40,6 +40,8 @@ const integrationAuthKeys = {
}) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const, }) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const,
getIntegrationAuthGithubOrgs: (integrationAuthId: string) => getIntegrationAuthGithubOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthGithubOrgs"] as const, [{ integrationAuthId }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) =>
[{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) => getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const, [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
getIntegrationAuthQoveryProjects: ({ getIntegrationAuthQoveryProjects: ({
@@ -186,6 +188,22 @@ const fetchIntegrationAuthGithubOrgs = async (integrationAuthId: string) => {
return orgs; return orgs;
}; };
const fetchIntegrationAuthGithubEnvs = async (
integrationAuthId: string,
repoName: string,
repoOwner: string
) => {
if (!repoName || !repoOwner) return [];
const {
data: { envs }
} = await apiRequest.get<{ envs: Array<{ name: string; envId: string }> }>(
`/api/v1/integration-auth/${integrationAuthId}/github/envs?repoName=${repoName}&repoOwner=${repoOwner}`
);
return envs;
};
const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => { const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
const { const {
data: { orgs } data: { orgs }
@@ -485,6 +503,22 @@ export const useGetIntegrationAuthGithubOrgs = (integrationAuthId: string) => {
}); });
}; };
export const useGetIntegrationAuthGithubEnvs = (
integrationAuthId: string,
repoName: string,
repoOwner: string
) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthGithubEnvs(
integrationAuthId,
repoName,
repoOwner
),
queryFn: () => fetchIntegrationAuthGithubEnvs(integrationAuthId, repoName, repoOwner),
enabled: true
});
};
export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => { export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId),
+187 -126
View File
@@ -41,6 +41,7 @@ import {
useCreateIntegration, useCreateIntegration,
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetIntegrationAuthGithubEnvs,
useGetIntegrationAuthGithubOrgs, useGetIntegrationAuthGithubOrgs,
useGetWorkspaceById useGetWorkspaceById
} from "@app/hooks/api"; } from "@app/hooks/api";
@@ -50,13 +51,8 @@ enum TabSections {
Options = "options" Options = "options"
} }
const targetEnv = [ const targetEnv = ["github-repo", "github-org", "github-env"] as const;
"github-repo", type TargetEnv = (typeof targetEnv)[number];
"github-org",
"github-env"
] as const;
type TargetEnv = typeof targetEnv[number];
const schema = yup.object({ const schema = yup.object({
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"), selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
@@ -64,22 +60,31 @@ const schema = yup.object({
secretSuffix: yup.string().trim().optional(), secretSuffix: yup.string().trim().optional(),
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(), scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
repoIds: yup
.array(yup.string().required()) repoIds: yup.mixed().when("scope", {
.min(1, "Select atleast one repo") // .min() not working showing error for empty array is: "github-repo",
.optional(), then: yup.array(yup.string().required()).min(1, "Select atleast one repositories")
repoId: yup }),
.string()
.optional(), repoName: yup.mixed().when("scope", {
is: "github-env",
envId: yup then: yup.string().required("Repository is required")
.string() }),
.optional(),
repoOwner: yup.mixed().when("scope", {
orgId: yup is: "github-env",
.string() then: yup.string().required("Repository is required")
.optional(), }),
envId: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Environment is required")
}),
orgId: yup.mixed().when("scope", {
is: "github-org",
then: yup.string().required("Organization is required")
})
}); });
type FormData = yup.InferType<typeof schema>; type FormData = yup.InferType<typeof schema>;
@@ -89,33 +94,41 @@ export default function GitHubCreateIntegrationPage() {
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const integrationAuthId =
(queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? "";
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId);
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } = const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
useGetIntegrationAuthApps({ useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "" integrationAuthId
}); });
const { data: integrationAuthOrgs } = const { data: integrationAuthOrgs } = useGetIntegrationAuthGithubOrgs(
useGetIntegrationAuthGithubOrgs(integrationAuthId as string); integrationAuthId as string
);
const { control, handleSubmit, watch, setValue } = useForm<FormData>({ const { control, handleSubmit, watch, setValue } = useForm<FormData>({
resolver: yupResolver(schema), resolver: yupResolver(schema),
defaultValues: { defaultValues: {
selectedSourceEnvironment: "",
secretPath: "/", secretPath: "/",
repoIds: [], scope: "github-repo",
secretSuffix: "", repoIds: []
scope: "github-repo"
} }
}); });
const scope = watch("scope"); const scope = watch("scope");
const repoIds = watch("repoIds"); const repoIds = watch("repoIds");
const repoName = watch("repoName");
const repoOwner = watch("repoOwner");
const { data: integrationAuthGithubEnvs } = useGetIntegrationAuthGithubEnvs(
integrationAuthId as string,
repoName,
repoOwner
);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -124,61 +137,93 @@ export default function GitHubCreateIntegrationPage() {
} }
}, [workspace]); }, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setValue("repoIds", [String(integrationAuthApps[0].appId)]);
} else {
setValue("repoIds", ["none"]);
}
}
}, [integrationAuthApps]);
const onFormSubmit = async (data: FormData) => { const onFormSubmit = async (data: FormData) => {
try { try {
setIsLoading(true); setIsLoading(true);
if (!integrationAuth?.id) return; if (!integrationAuth?.id) return;
const targetApps = integrationAuthApps?.filter((integrationAuthApp) => switch (data.scope) {
data.repoIds?.includes(String(integrationAuthApp.appId)) case "github-repo": {
); const targetApps = integrationAuthApps?.filter((integrationAuthApp) =>
data.repoIds?.includes(String(integrationAuthApp.appId))
);
if (!targetApps) return; if (!targetApps) return;
await Promise.all( await Promise.all(
targetApps.map(async (targetApp) => { targetApps.map(async (targetApp) => {
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
scope: data.scope,
secretPath: data.secretPath,
sourceEnvironment: data.selectedSourceEnvironment,
app: targetApp.name, // repo name
owner: targetApp.owner, // repo owner
metadata: {
secretSuffix: data.secretSuffix
}
});
})
);
break;
}
case "github-org":
await mutateAsync({ await mutateAsync({
integrationAuthId: integrationAuth?.id, integrationAuthId: integrationAuth?.id,
isActive: true, isActive: true,
app: targetApp.name,
owner: targetApp.owner,
secretPath: data.secretPath, secretPath: data.secretPath,
sourceEnvironment: data.selectedSourceEnvironment, sourceEnvironment: data.selectedSourceEnvironment,
scope: data.scope,
targetServiceId: data.orgId, // github org id
metadata: { metadata: {
secretSuffix: data.secretSuffix secretSuffix: data.secretSuffix
} }
}); });
}) break;
);
case "github-env":
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
secretPath: data.secretPath,
sourceEnvironment: data.selectedSourceEnvironment,
scope: data.scope,
app: repoName, // repo name // TODO: CHANGE THIS STATE INTO YUP
owner: repoOwner, // repo owner
targetEnvironmentId: data.envId, // github environment id
metadata: {
secretSuffix: data.secretSuffix
}
});
break;
default:
throw new Error("Invalid scope");
}
setIsLoading(false); setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`); router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
let errorMessage: string = "Something went wrong!";
if (axios.isAxiosError(err)) { if (axios.isAxiosError(err)) {
const { message } = err?.response?.data as { message: string }; const { message } = err?.response?.data as { message: string };
createNotification({ errorMessage = message;
text: message,
type: "error"
});
} }
createNotification({
text: errorMessage,
type: "error"
});
setIsLoading(false); setIsLoading(false);
} }
}; };
return integrationAuth && workspace && integrationAuthApps ? ( return integrationAuth && workspace && integrationAuthApps ? (
<div className="flex h-full w-full flex-col items-center justify-center"> <div className="flex w-full flex-col items-center justify-center py-4">
<Head> <Head>
<title>Set Up GitHub Integration</title> <title>Set Up GitHub Integration</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
@@ -239,7 +284,7 @@ export default function GitHubCreateIntegrationPage() {
> >
<Select <Select
defaultValue={field.value} defaultValue={field.value}
onValueChange={(e) => onChange(e)} onValueChange={onChange}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
@@ -267,19 +312,14 @@ export default function GitHubCreateIntegrationPage() {
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="scope" name="scope"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
label="Scope"
errorText={error?.message}
isError={Boolean(error)}
>
<Select <Select
defaultValue={field.value} defaultValue={field.value}
onValueChange={(e) => onChange(e)} onValueChange={onChange}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
<SelectItem value="github-repo">Github Repositories</SelectItem> <SelectItem value="github-repo">Github Repositories</SelectItem>
@@ -289,17 +329,15 @@ export default function GitHubCreateIntegrationPage() {
</FormControl> </FormControl>
)} )}
/> />
{scope === "github-repo" && (
{scope === "github-repo" && repoIds && (
<Controller <Controller
control={control} control={control}
name="repoIds" name="repoIds"
render={({ field: { onChange }}) => ( render={({ field: { onChange }, fieldState: { error } }) => (
<FormControl <FormControl
label="GitHub Repo" label="Repositories"
// BUG: yup.min() not working as expected needs to be fixed isError={Boolean(error?.message)}
errorText="Atleast one repo is required" errorText={error?.message}
isError={repoIds?.length === 0}
> >
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -335,7 +373,8 @@ export default function GitHubCreateIntegrationPage() {
if (repoIds.includes(String(integrationAuthApp.appId))) { if (repoIds.includes(String(integrationAuthApp.appId))) {
onChange( onChange(
repoIds.filter( repoIds.filter(
(appId) => appId !== String(integrationAuthApp.appId) (appId: string) =>
appId !== String(integrationAuthApp.appId)
) )
); );
} else { } else {
@@ -368,68 +407,73 @@ export default function GitHubCreateIntegrationPage() {
</FormControl> </FormControl>
)} )}
/> />
)} )}
{scope === "github-org" && ( {scope === "github-org" && (
<Controller <Controller
control={control} control={control}
name="orgId" name="orgId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Organization" label="Organization"
errorText={error?.message} errorText={
isError={Boolean(error)} integrationAuthOrgs?.length ? error?.message : "No organizations found"
> }
<Select isError={Boolean(integrationAuthOrgs?.length && error?.message)}
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
> >
{integrationAuthOrgs && integrationAuthOrgs.map(({name, orgId}) => ( <Select
<SelectItem defaultValue={field.value}
key={`github-organization-${orgId}`} onValueChange={onChange}
value={orgId} className="w-full border border-mineshaft-500"
> >
{name} {integrationAuthOrgs &&
</SelectItem> integrationAuthOrgs.map(({ name, orgId }) => (
<SelectItem key={`github-organization-${orgId}`} value={orgId}>
))} {name}
</Select> </SelectItem>
</FormControl> ))}
)} </Select>
/> </FormControl>
)}
/>
)} )}
{scope === "github-env" && ( {scope === "github-env" && (
<Controller <Controller
control={control} control={control}
name="repoId" name="repoName"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Repositories" label="Repository"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
<Select <Select
defaultValue={field.value} defaultValue={field.value}
onValueChange={(e) => onChange(e)} onValueChange={(e) => {
setValue("repoName", e);
setValue(
"repoOwner",
(integrationAuthApps.find((app) => app.name === e)
?.owner as string) || ""
);
onChange(e);
}}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
{integrationAuthApps.length > 0 ? ( {integrationAuthApps?.length ? (
integrationAuthApps.map((integrationAuthApp) => { integrationAuthApps.map((app) => {
return ( return (
<SelectItem <SelectItem
value={integrationAuthApp.appId as string} value={app.name as string}
key={`repos-id-${integrationAuthApp.appId}`} key={`repo-id-${app.appId}`}
className="w-[28.4rem] text-sm" className="w-[28.4rem] text-sm"
> >
{integrationAuthApp.name} {app.name}
</SelectItem> </SelectItem>
); );
}) })
) : ( ) : (
<div /> <div />
) )}
}
</Select> </Select>
</FormControl> </FormControl>
)} )}
@@ -442,15 +486,33 @@ export default function GitHubCreateIntegrationPage() {
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Environment" label="Environment"
errorText={error?.message} errorText={
isError={Boolean(error)} integrationAuthGithubEnvs?.length
? error?.message
: "No Environment found"
}
isError={Boolean(integrationAuthGithubEnvs?.length || error?.message)}
> >
<Select <Select
defaultValue={field.value} defaultValue={field.value}
onValueChange={(e) => onChange(e)} onValueChange={onChange}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
<SelectItem value="github-repo">Select Environment</SelectItem> {integrationAuthGithubEnvs?.length ? (
integrationAuthGithubEnvs.map((githubEnv) => {
return (
<SelectItem
value={githubEnv.name as string}
key={`env-id-${githubEnv.envId}`}
className="w-[28.4rem] text-sm"
>
{githubEnv.name}
</SelectItem>
);
})
) : (
<div />
)}
</Select> </Select>
</FormControl> </FormControl>
)} )}
@@ -493,7 +555,6 @@ export default function GitHubCreateIntegrationPage() {
variant="outline_bg" variant="outline_bg"
className="mb-6" className="mb-6"
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0 || repoIds?.length === 0}
> >
Create Integration Create Integration
</Button> </Button>