feat(integrations): visibility support for github integration

This commit is contained in:
Daniel Hougaard
2024-09-03 04:40:59 +04:00
parent b2d2297914
commit 4a14c3efd2
3 changed files with 181 additions and 27 deletions
@@ -71,6 +71,8 @@ export const useCreateIntegration = () => {
key: string; key: string;
value: string; value: string;
}[]; }[];
githubVisibility?: string;
githubVisibilityRepoIds?: string[];
kmsKeyId?: string; kmsKeyId?: string;
shouldDisableDelete?: boolean; shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean; shouldMaskSecrets?: boolean;
@@ -34,6 +34,9 @@ export type TIntegration = {
syncMessage?: string; syncMessage?: string;
__v: number; __v: number;
metadata?: { metadata?: {
githubVisibility?: string;
githubVisibilityRepoIds?: string[];
secretSuffix?: string; secretSuffix?: string;
syncBehavior?: IntegrationSyncBehavior; syncBehavior?: IntegrationSyncBehavior;
mappingBehavior?: IntegrationMappingBehavior; mappingBehavior?: IntegrationMappingBehavior;
+176 -27
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import Head from "next/head"; import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
@@ -53,6 +53,21 @@ enum TabSections {
Options = "options" Options = "options"
} }
const secretsVisibility = [
{
value: "selected",
label: "Select repositories"
},
{
value: "all",
label: "All public repositories"
},
{
value: "private",
label: "All private repositories"
}
] as const;
const targetEnv = ["github-repo", "github-org", "github-env"] as const; const targetEnv = ["github-repo", "github-org", "github-env"] as const;
type TargetEnv = (typeof targetEnv)[number]; type TargetEnv = (typeof targetEnv)[number];
@@ -63,9 +78,12 @@ const schema = yup.object({
shouldEnableDelete: yup.boolean().optional(), shouldEnableDelete: yup.boolean().optional(),
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(), scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
repoIds: yup.mixed().when("scope", { // Explanation: If scope is (github-repo) OR (github-org AND visibility is set to selected), then repoIds is required
is: "github-repo", repoIds: yup.mixed().when(["scope", "visibility"], {
then: yup.array(yup.string().required()).min(1, "Select at least one repositories") is: (scope: string, visibility: string) =>
scope === "github-repo" || (scope === "github-org" && visibility === "selected"),
then: yup.array(yup.string().required()).min(1, "Select at least one repository"),
otherwise: yup.mixed().notRequired()
}), }),
repoId: yup.mixed().when("scope", { repoId: yup.mixed().when("scope", {
@@ -91,6 +109,10 @@ const schema = yup.object({
orgId: yup.mixed().when("scope", { orgId: yup.mixed().when("scope", {
is: "github-org", is: "github-org",
then: yup.string().required("Organization is required") then: yup.string().required("Organization is required")
}),
visibility: yup.mixed().when("scope", {
is: "github-org",
then: yup.string().required("Visibility is required")
}) })
}); });
@@ -121,6 +143,7 @@ export default function GitHubCreateIntegrationPage() {
secretPath: "/", secretPath: "/",
scope: "github-repo", scope: "github-repo",
repoIds: [], repoIds: [],
visibility: "all",
shouldEnableDelete: false shouldEnableDelete: false
} }
}); });
@@ -130,6 +153,7 @@ export default function GitHubCreateIntegrationPage() {
const repoIds = watch("repoIds"); const repoIds = watch("repoIds");
const repoName = watch("repoName"); const repoName = watch("repoName");
const repoOwner = watch("repoOwner"); const repoOwner = watch("repoOwner");
const selectedOrgId = watch("orgId");
const { data: integrationAuthGithubEnvs } = useGetIntegrationAuthGithubEnvs( const { data: integrationAuthGithubEnvs } = useGetIntegrationAuthGithubEnvs(
integrationAuthId as string, integrationAuthId as string,
@@ -196,6 +220,8 @@ export default function GitHubCreateIntegrationPage() {
scope: data.scope, scope: data.scope,
owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name, owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
metadata: { metadata: {
githubVisibility: data.visibility,
githubVisibilityRepoIds: data.repoIds,
secretSuffix: data.secretSuffix, secretSuffix: data.secretSuffix,
shouldEnableDelete: data.shouldEnableDelete shouldEnableDelete: data.shouldEnableDelete
} }
@@ -242,6 +268,15 @@ export default function GitHubCreateIntegrationPage() {
} }
}; };
const selectedOrganization = useMemo(() => {
if (!integrationAuthApps) return null;
return integrationAuthApps.filter(
(authApp) =>
integrationAuthOrgs?.find((e) => e.orgId === selectedOrgId)?.name === authApp.owner
);
}, [selectedOrgId, integrationAuthApps]);
return integrationAuth && workspace && integrationAuthApps ? ( return integrationAuth && workspace && integrationAuthApps ? (
<div className="flex h-full w-full flex-col items-center justify-center py-4"> <div className="flex h-full w-full flex-col items-center justify-center py-4">
<Head> <Head>
@@ -339,7 +374,10 @@ export default function GitHubCreateIntegrationPage() {
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}> <FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
<Select <Select
defaultValue={field.value} defaultValue={field.value}
onValueChange={onChange} onValueChange={(e) => {
setValue("repoIds", []);
onChange(e);
}}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
> >
<SelectItem value="github-org">Organization</SelectItem> <SelectItem value="github-org">Organization</SelectItem>
@@ -430,32 +468,143 @@ export default function GitHubCreateIntegrationPage() {
/> />
)} )}
{scope === "github-org" && ( {scope === "github-org" && (
<Controller <>
control={control} <Controller
name="orgId" control={control}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( name="orgId"
<FormControl render={({ field: { onChange, ...field }, fieldState: { error } }) => (
label="Organization" <FormControl
errorText={ label="Organization"
integrationAuthOrgs?.length ? error?.message : "No organizations found" errorText={
} integrationAuthOrgs?.length ? error?.message : "No organizations found"
isError={Boolean(integrationAuthOrgs?.length && error?.message)} }
> isError={Boolean(integrationAuthOrgs?.length && error?.message)}
<Select
value={field.value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
> >
{integrationAuthOrgs && <Select
integrationAuthOrgs.map(({ name, orgId }) => ( value={field.value}
<SelectItem key={`github-organization-${orgId}`} value={orgId}> onValueChange={onChange}
{name} className="w-full border border-mineshaft-500"
>
{integrationAuthOrgs &&
integrationAuthOrgs.map(({ name, orgId }) => (
<SelectItem key={`github-organization-${orgId}`} value={orgId}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="visibility"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
tooltipText="Select which type of repositories that the secrets should be synced to."
label="Visibility"
errorText={error?.message}
isError={Boolean(error?.message)}
>
<Select
value={field.value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
>
{secretsVisibility.map(({ label, value }) => (
<SelectItem key={`github-visibility-${value}`} value={value}>
{label}
</SelectItem> </SelectItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
)}
/>
{watch("visibility") === "selected" && (
<Controller
control={control}
name="repoIds"
render={({ field: { onChange }, fieldState: { error } }) => (
<FormControl
label="Selected Repositories"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<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?.reduce(
(acc, { appId, name, owner }) =>
repoIds[0] === appId ? `${owner}/${name}` : acc,
""
)
: `${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"
>
{selectedOrganization ? (
selectedOrganization.map((integrationAuthApp) => {
const isSelected = repoIds.includes(
String(integrationAuthApp.appId)
);
return (
<DropdownMenuItem
onClick={() => {
if (repoIds.includes(String(integrationAuthApp.appId))) {
onChange(
repoIds.filter(
(appId: string) =>
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.owner}/{integrationAuthApp.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
)} )}
/> </>
)} )}
{scope === "github-env" && ( {scope === "github-env" && (
<Controller <Controller