Update github form ui add scope and init org api

This commit is contained in:
Salman
2024-02-14 04:21:50 +05:30
parent c8cfb43316
commit df459d456a
7 changed files with 325 additions and 88 deletions
@@ -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({ server.route({
url: "/:integrationAuthId/qovery/orgs", url: "/:integrationAuthId/qovery/orgs",
method: "GET", method: "GET",
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Octokit } from "@octokit/rest";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -23,6 +24,7 @@ import {
TIntegrationAuthAppsDTO, TIntegrationAuthAppsDTO,
TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthNorthflankSecretGroupDTO, TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryEnvironmentsDTO,
TIntegrationAuthQoveryOrgsDTO, TIntegrationAuthQoveryOrgsDTO,
@@ -381,6 +383,33 @@ export const integrationAuthServiceFactory = ({
return []; 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 getQoveryOrgs = async ({ actorId, actor, actorOrgId, id }: TIntegrationAuthQoveryOrgsDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id); const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" }); if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
@@ -911,6 +940,7 @@ export const integrationAuthServiceFactory = ({
getIntegrationApps, getIntegrationApps,
getVercelBranches, getVercelBranches,
getApps, getApps,
getGithubOrgs,
getChecklyGroups, getChecklyGroups,
getQoveryApps, getQoveryApps,
getQoveryEnvs, getQoveryEnvs,
@@ -44,6 +44,10 @@ export type TIntegrationAuthChecklyGroupsDTO = {
accountId: string; accountId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthGithubOrgsDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryOrgsDTO = { export type TIntegrationAuthQoveryOrgsDTO = {
id: string; id: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
+12
View File
@@ -1869,6 +1869,18 @@ paths:
responses: responses:
'200': '200':
description: OK 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: /api/v1/integration-auth/{integrationAuthId}/qovery/orgs:
get: get:
description: '' description: ''
@@ -6,6 +6,7 @@ export {
useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetIntegrationAuthChecklyGroups, useGetIntegrationAuthChecklyGroups,
useGetIntegrationAuthGithubOrgs,
useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthNorthflankSecretGroups,
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices, useGetIntegrationAuthRailwayServices,
@@ -38,6 +38,8 @@ const integrationAuthKeys = {
integrationAuthId: string; integrationAuthId: string;
accountId: string; accountId: string;
}) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const, }) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const,
getIntegrationAuthGithubOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) => getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const, [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
getIntegrationAuthQoveryProjects: ({ getIntegrationAuthQoveryProjects: ({
@@ -174,6 +176,16 @@ const fetchIntegrationAuthVercelBranches = async ({
return branches; 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 fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
const { const {
data: { orgs } 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) => { export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthQoveryOrgs(integrationAuthId),
+226 -88
View File
@@ -20,8 +20,6 @@ import queryString from "query-string";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { useCreateIntegration } from "@app/hooks/api";
import { import {
Button, Button,
Card, Card,
@@ -38,26 +36,50 @@ import {
TabList, TabList,
TabPanel, TabPanel,
Tabs Tabs
} from "../../../components/v2"; } from "@app/components/v2";
import { import {
useCreateIntegration,
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById,
} from "../../../hooks/api/integrationAuth"; useGetIntegrationAuthGithubOrgs,
import { useGetWorkspaceById } from "../../../hooks/api/workspace"; useGetWorkspaceById
} from "@app/hooks/api";
enum TabSections { enum TabSections {
Connection = "connection", Connection = "connection",
Options = "options" Options = "options"
} }
const targetEnv = [
"github-repo",
"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"),
secretPath: yup.string().trim().required("Secrets Path is required"), secretPath: yup.string().trim().required("Secrets Path is required"),
targetAppIds: yup secretSuffix: yup.string().trim().optional(),
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
repoIds: yup
.array(yup.string().required()) .array(yup.string().required())
.min(1, "Select atleast one repo") // .min() not working showing error for empty array .min(1, "Select atleast one repo") // .min() not working showing error for empty array
.required("Select atleast one repo"), .optional(),
secretSuffix: yup.string().trim().optional() repoId: yup
.string()
.optional(),
envId: yup
.string()
.optional(),
orgId: yup
.string()
.optional(),
}); });
type FormData = yup.InferType<typeof schema>; type FormData = yup.InferType<typeof schema>;
@@ -75,19 +97,25 @@ export default function GitHubCreateIntegrationPage() {
useGetIntegrationAuthApps({ useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "" integrationAuthId: (integrationAuthId as string) ?? ""
}); });
const { data: integrationAuthOrgs } =
useGetIntegrationAuthGithubOrgs(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: "", selectedSourceEnvironment: "",
secretPath: "/", secretPath: "/",
targetAppIds: [], repoIds: [],
secretSuffix: "" secretSuffix: "",
scope: "github-repo"
} }
}); });
const targetAppIds = watch("targetAppIds"); const scope = watch("scope");
const repoIds = watch("repoIds");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
@@ -99,9 +127,9 @@ export default function GitHubCreateIntegrationPage() {
useEffect(() => { useEffect(() => {
if (integrationAuthApps) { if (integrationAuthApps) {
if (integrationAuthApps.length > 0) { if (integrationAuthApps.length > 0) {
setValue("targetAppIds", [String(integrationAuthApps[0].appId)]); setValue("repoIds", [String(integrationAuthApps[0].appId)]);
} else { } else {
setValue("targetAppIds", ["none"]); setValue("repoIds", ["none"]);
} }
} }
}, [integrationAuthApps]); }, [integrationAuthApps]);
@@ -113,7 +141,7 @@ export default function GitHubCreateIntegrationPage() {
if (!integrationAuth?.id) return; if (!integrationAuth?.id) return;
const targetApps = integrationAuthApps?.filter((integrationAuthApp) => const targetApps = integrationAuthApps?.filter((integrationAuthApp) =>
data.targetAppIds.includes(String(integrationAuthApp.appId)) data.repoIds?.includes(String(integrationAuthApp.appId))
); );
if (!targetApps) return; if (!targetApps) return;
@@ -217,7 +245,7 @@ export default function GitHubCreateIntegrationPage() {
{workspace?.environments.map((sourceEnvironment) => ( {workspace?.environments.map((sourceEnvironment) => (
<SelectItem <SelectItem
value={sourceEnvironment.slug} value={sourceEnvironment.slug}
key={`github-environment-${sourceEnvironment.slug}`} key={`source-environment-${sourceEnvironment.slug}`}
> >
{sourceEnvironment.name} {sourceEnvironment.name}
</SelectItem> </SelectItem>
@@ -226,7 +254,6 @@ export default function GitHubCreateIntegrationPage() {
</FormControl> </FormControl>
)} )}
/> />
<Controller <Controller
control={control} control={control}
name="secretPath" name="secretPath"
@@ -243,81 +270,192 @@ export default function GitHubCreateIntegrationPage() {
<Controller <Controller
control={control} control={control}
name="targetAppIds" name="scope"
render={({ field: { onChange }}) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="GitHub Repo" label="Scope"
// BUG: yup.min() not working as expected needs to be fixed errorText={error?.message}
errorText="Atleast one repo is required" isError={Boolean(error)}
isError={targetAppIds?.length === 0}
> >
<DropdownMenu> <Select
<DropdownMenuTrigger asChild> defaultValue={field.value}
{integrationAuthApps.length > 0 ? ( onValueChange={(e) => onChange(e)}
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200"> className="w-full border border-mineshaft-500"
{targetAppIds.length === 1 >
? integrationAuthApps?.find( <SelectItem value="github-repo">Github Repositories</SelectItem>
(integrationAuthApp) => <SelectItem value="github-org">Github Organization</SelectItem>
targetAppIds[0] === String(integrationAuthApp.appId) <SelectItem value="github-env">Github Environment</SelectItem>
)?.name </Select>
: `${targetAppIds.length} repositories selected`}
<FontAwesomeIcon icon={faAngleDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No repositories found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80 overflow-y-scroll"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => {
const isSelected = targetAppIds.includes(
String(integrationAuthApp.appId)
);
return (
<DropdownMenuItem
onClick={() => {
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 ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{integrationAuthApp.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl> </FormControl>
)} )}
/> />
{scope === "github-repo" && repoIds && (
<Controller
control={control}
name="repoIds"
render={({ field: { onChange }}) => (
<FormControl
label="GitHub Repo"
// BUG: yup.min() not working as expected needs to be fixed
errorText="Atleast one repo is required"
isError={repoIds?.length === 0}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{integrationAuthApps.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{repoIds.length === 1
? integrationAuthApps?.find(
(integrationAuthApp) =>
repoIds[0] === String(integrationAuthApp.appId)
)?.name
: `${repoIds.length} repositories selected`}
<FontAwesomeIcon icon={faAngleDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No repositories found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80 overflow-y-scroll"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => {
const isSelected = repoIds.includes(
String(integrationAuthApp.appId)
);
return (
<DropdownMenuItem
onClick={() => {
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 ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
{integrationAuthApp.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)}
{scope === "github-org" && (
<Controller
control={control}
name="orgId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Organization"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
>
{integrationAuthOrgs && integrationAuthOrgs.map(({name, orgId}) => (
<SelectItem
key={`github-organization-${orgId}`}
value={orgId}
>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
{scope === "github-env" && (
<Controller
control={control}
name="repoId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Repositories"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => {
return (
<SelectItem
value={integrationAuthApp.appId as string}
key={`repos-id-${integrationAuthApp.appId}`}
className="w-[28.4rem] text-sm"
>
{integrationAuthApp.name}
</SelectItem>
);
})
) : (
<div />
)
}
</Select>
</FormControl>
)}
/>
)}
{scope === "github-env" && (
<Controller
control={control}
name="envId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
onValueChange={(e) => onChange(e)}
className="w-full border border-mineshaft-500"
>
<SelectItem value="github-repo">Select Environment</SelectItem>
</Select>
</FormControl>
)}
/>
)}
</motion.div> </motion.div>
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Options}> <TabPanel value={TabSections.Options}>
@@ -355,7 +493,7 @@ export default function GitHubCreateIntegrationPage() {
variant="outline_bg" variant="outline_bg"
className="mb-6" className="mb-6"
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0 || targetAppIds.length === 0} isDisabled={integrationAuthApps.length === 0 || repoIds?.length === 0}
> >
Create Integration Create Integration
</Button> </Button>