From df459d456aac06b194693d484110e4a563e0fe28 Mon Sep 17 00:00:00 2001 From: Salman Date: Wed, 14 Feb 2024 04:21:50 +0530 Subject: [PATCH] Update github form ui add scope and init org api --- .../routes/v1/integration-auth-router.ts | 32 ++ .../integration-auth-service.ts | 30 ++ .../integration-auth-types.ts | 4 + docs/spec.yaml | 12 + .../src/hooks/api/integrationAuth/index.tsx | 1 + .../src/hooks/api/integrationAuth/queries.tsx | 20 ++ .../src/pages/integrations/github/create.tsx | 314 +++++++++++++----- 7 files changed, 325 insertions(+), 88 deletions(-) diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 4d7aa1b1e..871b2ec65 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -343,6 +343,38 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) } }); + server.route({ + url: "/:integrationAuthId/github/orgs", + method: "GET", + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + orgs: z.object({ name: z.string(), orgId: z.string() }).array() + }) + } + }, + handler: async (req) => { + try { + const orgs = await server.services.integrationAuth.getGithubOrgs({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId + }); + if (!orgs) throw new Error("No organization found."); + + return { orgs: orgs || [] }; + } catch (e) { + console.error(e); + return { orgs: [] }; + } + } + }); + server.route({ url: "/:integrationAuthId/qovery/orgs", method: "GET", diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index 0b3f9b4c3..c158567c3 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { Octokit } from "@octokit/rest"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -23,6 +24,7 @@ import { TIntegrationAuthAppsDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, + TIntegrationAuthGithubOrgsDTO, TIntegrationAuthNorthflankSecretGroupDTO, TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryOrgsDTO, @@ -381,6 +383,33 @@ export const integrationAuthServiceFactory = ({ return []; }; + const getGithubOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthGithubOrgsDTO) => { + 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 } = await octokit.request("GET /organizations", { + headers: { + "X-GitHub-Api-Version": "2022-11-28" + } + }); + if (!data) return []; + return data.map(({ login: name, id: orgId }) => ({ name, orgId })); + }; + const getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => { const integrationAuth = await integrationAuthDAL.findById(id); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); @@ -911,6 +940,7 @@ export const integrationAuthServiceFactory = ({ getIntegrationApps, getVercelBranches, getApps, + getGithubOrgs, getChecklyGroups, getQoveryApps, getQoveryEnvs, diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 34c5d995a..8cf20d747 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -44,6 +44,10 @@ export type TIntegrationAuthChecklyGroupsDTO = { accountId: string; } & Omit; +export type TIntegrationAuthGithubOrgsDTO = { + id: string; +} & Omit; + export type TIntegrationAuthQoveryOrgsDTO = { id: string; } & Omit; diff --git a/docs/spec.yaml b/docs/spec.yaml index c3d050395..d7442497a 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -1869,6 +1869,18 @@ paths: responses: '200': description: OK + /api/v1/integration-auth/{integrationAuthId}/github/orgs: + get: + description: '' + parameters: + - name: integrationAuthId + in: path + required: true + schema: + type: string + responses: + '200': + description: OK /api/v1/integration-auth/{integrationAuthId}/qovery/orgs: get: description: '' diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index e70ffdccb..358ed1a63 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -6,6 +6,7 @@ export { useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthById, useGetIntegrationAuthChecklyGroups, + useGetIntegrationAuthGithubOrgs, useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayServices, diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index 83dfb5d1a..eb675c628 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -38,6 +38,8 @@ const integrationAuthKeys = { integrationAuthId: string; accountId: string; }) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const, + getIntegrationAuthGithubOrgs: (integrationAuthId: string) => + [{ integrationAuthId }, "integrationAuthGithubOrgs"] as const, getIntegrationAuthQoveryOrgs: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const, getIntegrationAuthQoveryProjects: ({ @@ -174,6 +176,16 @@ const fetchIntegrationAuthVercelBranches = async ({ return branches; }; +const fetchIntegrationAuthGithubOrgs = async (integrationAuthId: string) => { + const { + data: { orgs } + } = await apiRequest.get<{ orgs: Org[] }>( + `/api/v1/integration-auth/${integrationAuthId}/github/orgs` + ); + + return orgs; +}; + const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => { const { data: { orgs } @@ -465,6 +477,14 @@ export const useGetIntegrationAuthChecklyGroups = ({ }); }; +export const useGetIntegrationAuthGithubOrgs = (integrationAuthId: string) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthGithubOrgs(integrationAuthId), + queryFn: () => fetchIntegrationAuthGithubOrgs(integrationAuthId), + enabled: true + }); +}; + export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId), diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx index f09602384..6aea82cf3 100644 --- a/frontend/src/pages/integrations/github/create.tsx +++ b/frontend/src/pages/integrations/github/create.tsx @@ -20,8 +20,6 @@ import queryString from "query-string"; import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import { useCreateIntegration } from "@app/hooks/api"; - import { Button, Card, @@ -38,26 +36,50 @@ import { TabList, TabPanel, Tabs -} from "../../../components/v2"; +} from "@app/components/v2"; import { + useCreateIntegration, useGetIntegrationAuthApps, - useGetIntegrationAuthById -} from "../../../hooks/api/integrationAuth"; -import { useGetWorkspaceById } from "../../../hooks/api/workspace"; + useGetIntegrationAuthById, + useGetIntegrationAuthGithubOrgs, + useGetWorkspaceById +} from "@app/hooks/api"; enum TabSections { Connection = "connection", Options = "options" } +const targetEnv = [ + "github-repo", + "github-org", + "github-env" +] as const; +type TargetEnv = typeof targetEnv[number]; + + const schema = yup.object({ selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"), secretPath: yup.string().trim().required("Secrets Path is required"), - targetAppIds: yup + secretSuffix: yup.string().trim().optional(), + + scope: yup.mixed().oneOf(targetEnv.slice()).required(), + repoIds: yup .array(yup.string().required()) .min(1, "Select atleast one repo") // .min() not working showing error for empty array - .required("Select atleast one repo"), - secretSuffix: yup.string().trim().optional() + .optional(), + repoId: yup + .string() + .optional(), + + envId: yup + .string() + .optional(), + + orgId: yup + .string() + .optional(), + }); type FormData = yup.InferType; @@ -75,19 +97,25 @@ export default function GitHubCreateIntegrationPage() { useGetIntegrationAuthApps({ integrationAuthId: (integrationAuthId as string) ?? "" }); + + const { data: integrationAuthOrgs } = + useGetIntegrationAuthGithubOrgs(integrationAuthId as string); const { control, handleSubmit, watch, setValue } = useForm({ resolver: yupResolver(schema), defaultValues: { selectedSourceEnvironment: "", secretPath: "/", - targetAppIds: [], - secretSuffix: "" + repoIds: [], + secretSuffix: "", + scope: "github-repo" } }); - const targetAppIds = watch("targetAppIds"); + const scope = watch("scope"); + const repoIds = watch("repoIds"); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { @@ -99,9 +127,9 @@ export default function GitHubCreateIntegrationPage() { useEffect(() => { if (integrationAuthApps) { if (integrationAuthApps.length > 0) { - setValue("targetAppIds", [String(integrationAuthApps[0].appId)]); + setValue("repoIds", [String(integrationAuthApps[0].appId)]); } else { - setValue("targetAppIds", ["none"]); + setValue("repoIds", ["none"]); } } }, [integrationAuthApps]); @@ -113,7 +141,7 @@ export default function GitHubCreateIntegrationPage() { if (!integrationAuth?.id) return; const targetApps = integrationAuthApps?.filter((integrationAuthApp) => - data.targetAppIds.includes(String(integrationAuthApp.appId)) + data.repoIds?.includes(String(integrationAuthApp.appId)) ); if (!targetApps) return; @@ -217,7 +245,7 @@ export default function GitHubCreateIntegrationPage() { {workspace?.environments.map((sourceEnvironment) => ( {sourceEnvironment.name} @@ -226,7 +254,6 @@ export default function GitHubCreateIntegrationPage() { )} /> - ( + name="scope" + render={({ field: { onChange, ...field }, fieldState: { error } }) => ( - - - {integrationAuthApps.length > 0 ? ( -
- {targetAppIds.length === 1 - ? integrationAuthApps?.find( - (integrationAuthApp) => - targetAppIds[0] === String(integrationAuthApp.appId) - )?.name - : `${targetAppIds.length} repositories selected`} - -
- ) : ( -
- No repositories found -
- )} -
- - {integrationAuthApps.length > 0 ? ( - integrationAuthApps.map((integrationAuthApp) => { - const isSelected = targetAppIds.includes( - String(integrationAuthApp.appId) - ); - - return ( - { - if (targetAppIds.includes(String(integrationAuthApp.appId))) { - onChange( - targetAppIds.filter( - (appId) => appId !== String(integrationAuthApp.appId) - ) - ); - } else { - onChange([...targetAppIds, String(integrationAuthApp.appId)]); - } - }} - key={integrationAuthApp.appId} - icon={ - isSelected ? ( - - ) : ( -
- ) - } - iconPos="left" - className="w-[28.4rem] text-sm" - > - {integrationAuthApp.name} - - ); - }) - ) : ( -
- )} - - + )} /> + + {scope === "github-repo" && repoIds && ( + ( + + + + {integrationAuthApps.length > 0 ? ( +
+ {repoIds.length === 1 + ? integrationAuthApps?.find( + (integrationAuthApp) => + repoIds[0] === String(integrationAuthApp.appId) + )?.name + : `${repoIds.length} repositories selected`} + +
+ ) : ( +
+ No repositories found +
+ )} +
+ + {integrationAuthApps.length > 0 ? ( + integrationAuthApps.map((integrationAuthApp) => { + const isSelected = repoIds.includes( + String(integrationAuthApp.appId) + ); + + return ( + { + if (repoIds.includes(String(integrationAuthApp.appId))) { + onChange( + repoIds.filter( + (appId) => appId !== String(integrationAuthApp.appId) + ) + ); + } else { + onChange([...repoIds, String(integrationAuthApp.appId)]); + } + }} + key={`repos-id-${integrationAuthApp.appId}`} + icon={ + isSelected ? ( + + ) : ( +
+ ) + } + iconPos="left" + className="w-[28.4rem] text-sm" + > + {integrationAuthApp.name} + + ); + }) + ) : ( +
+ )} + + + + )} + /> + + )} + {scope === "github-org" && ( + ( + + + + )} + /> + )} + {scope === "github-env" && ( + ( + + + + )} + /> + )} + {scope === "github-env" && ( + ( + + + + )} + /> + )} @@ -355,7 +493,7 @@ export default function GitHubCreateIntegrationPage() { variant="outline_bg" className="mb-6" isLoading={isLoading} - isDisabled={integrationAuthApps.length === 0 || targetAppIds.length === 0} + isDisabled={integrationAuthApps.length === 0 || repoIds?.length === 0} > Create Integration