Merge pull request #1398 from Salman2301/feat-github-integration

Github Integrations
This commit is contained in:
BlackMagiq
2024-03-15 20:44:09 -07:00
committed by GitHub
15 changed files with 922 additions and 359 deletions
@@ -343,6 +343,66 @@ 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) => {
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 };
}
});
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 };
}
});
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";
@@ -24,6 +25,8 @@ import {
TIntegrationAuthAppsDTO, TIntegrationAuthAppsDTO,
TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO,
TIntegrationAuthGithubOrgsDTO,
TIntegrationAuthHerokuPipelinesDTO, TIntegrationAuthHerokuPipelinesDTO,
TIntegrationAuthNorthflankSecretGroupDTO, TIntegrationAuthNorthflankSecretGroupDTO,
TIntegrationAuthQoveryEnvironmentsDTO, TIntegrationAuthQoveryEnvironmentsDTO,
@@ -383,6 +386,72 @@ 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 /user/orgs", {
headers: {
"X-GitHub-Api-Version": "2022-11-28"
}
});
if (!data) return [];
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) => {
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" });
@@ -756,9 +825,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"
@@ -934,6 +1001,8 @@ export const integrationAuthServiceFactory = ({
getIntegrationApps, getIntegrationApps,
getVercelBranches, getVercelBranches,
getApps, getApps,
getGithubOrgs,
getGithubEnvs,
getChecklyGroups, getChecklyGroups,
getQoveryApps, getQoveryApps,
getQoveryEnvs, getQoveryEnvs,
@@ -44,6 +44,16 @@ export type TIntegrationAuthChecklyGroupsDTO = {
accountId: string; accountId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthGithubOrgsDTO = {
id: string;
} & 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">;
@@ -1110,98 +1110,176 @@ const syncSecretsGitHub = async ({
interface GitHubRepoKey { interface GitHubRepoKey {
key_id: string; key_id: string;
key: string; key: string;
id?: number | undefined;
url?: string | undefined;
title?: string | undefined;
created_at?: string | undefined;
} }
interface GitHubSecret { interface GitHubSecret {
name: string; name: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} visibility?: "all" | "private" | "selected";
selected_repositories_url?: string | undefined;
interface GitHubSecretRes {
[index: string]: GitHubSecret;
} }
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken auth: accessToken
}); });
// const user = (await octokit.request('GET /user', {})).data; enum GithubScope {
const repoPublicKey: GitHubRepoKey = ( Repo = "github-repo",
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", { Org = "github-org",
owner: integration.owner as string, Env = "github-env"
repo: integration.app as string }
})
).data; let repoPublicKey: GitHubRepoKey;
switch (integration.scope) {
case GithubScope.Org: {
const { data } = await octokit.request("GET /orgs/{org}/actions/secrets/public-key", {
org: integration.owner as string
});
repoPublicKey = data;
break;
}
case GithubScope.Env: {
const { data } = await octokit.request(
"GET /repositories/{repository_id}/environments/{environment_name}/secrets/public-key",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string
}
);
repoPublicKey = data;
break;
}
default: {
const { data } = await octokit.request("GET /repos/{owner}/{repo}/actions/secrets/public-key", {
owner: integration.owner as string,
repo: integration.app as string
});
repoPublicKey = data;
break;
}
}
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
let encryptedSecrets: GitHubSecretRes = ( let encryptedSecrets: GitHubSecret[];
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner as string,
repo: integration.app as string
})
).data.secrets.reduce(
(obj, secret) => ({
...obj,
[secret.name]: secret
}),
{}
);
encryptedSecrets = Object.keys(encryptedSecrets).reduce( switch (integration.scope) {
( case GithubScope.Org: {
result: { encryptedSecrets = (
[key: string]: GitHubSecret; await octokit.request("GET /orgs/{org}/actions/secrets", {
}, org: integration.owner as string
key })
) => { ).data.secrets;
if ( break;
(appendices?.prefix !== undefined ? key.startsWith(appendices?.prefix) : true) && }
(appendices?.suffix !== undefined ? key.endsWith(appendices?.suffix) : true) case GithubScope.Env: {
) { encryptedSecrets = (
result[key] = encryptedSecrets[key]; await octokit.request("GET /repositories/{repository_id}/environments/{environment_name}/secrets", {
} repository_id: Number(integration.appId),
return result; environment_name: integration.targetEnvironmentId as string
}, })
{} ).data.secrets;
); break;
}
await Promise.all( default: {
Object.keys(encryptedSecrets).map(async (key) => { encryptedSecrets = (
if (!(key in secrets)) { await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
return octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string, owner: integration.owner as string,
repo: integration.app as string, repo: integration.app as string
secret_name: key })
}); ).data.secrets;
break;
}
}
for await (const encryptedSecret of encryptedSecrets) {
if (
!(encryptedSecret.name in secrets) &&
!(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) &&
!(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix))
) {
switch (integration.scope) {
case GithubScope.Org: {
await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", {
org: integration.owner as string,
secret_name: encryptedSecret.name
});
break;
}
case GithubScope.Env: {
await octokit.request(
"DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: encryptedSecret.name
}
);
break;
}
default: {
await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: encryptedSecret.name
});
break;
}
} }
}) }
); }
await Promise.all( await sodium.ready.then(async () => {
Object.keys(secrets).map((key) => { for await (const key of Object.keys(secrets)) {
// let encryptedSecret; // convert secret & base64 key to Uint8Array.
return sodium.ready.then(async () => { const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
// convert secret & base64 key to Uint8Array. const binsec = sodium.from_string(secrets[key].value);
const binkey = sodium.from_base64(repoPublicKey.key, sodium.base64_variants.ORIGINAL);
const binsec = sodium.from_string(secrets[key].value);
// encrypt secret using libsodium // encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey); const encBytes = sodium.crypto_box_seal(binsec, binkey);
// convert encrypted Uint8Array to base64 // convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL); const encryptedSecret = sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL);
await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", { switch (integration.scope) {
owner: integration.owner as string, case GithubScope.Org:
repo: integration.app as string, await octokit.request("PUT /orgs/{org}/actions/secrets/{secret_name}", {
secret_name: key, org: integration.owner as string,
encrypted_value: encryptedSecret, secret_name: key,
key_id: repoPublicKey.key_id visibility: "all",
}); encrypted_value: encryptedSecret,
}); key_id: repoPublicKey.key_id
}) });
); break;
case GithubScope.Env:
await octokit.request(
"PUT /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}",
{
repository_id: Number(integration.appId),
environment_name: integration.targetEnvironmentId as string,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
}
);
break;
default:
await octokit.request("PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", {
owner: integration.owner as string,
repo: integration.app as string,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
});
break;
}
}
});
}; };
/** /**
Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 649 KiB

+24 -12
View File
@@ -3,17 +3,14 @@ title: "GitHub Actions"
description: "How to sync secrets from Infisical to GitHub Actions" description: "How to sync secrets from Infisical to GitHub Actions"
--- ---
Infisical lets you sync secrets to GitHub at the organization-level, repository-level, and repository environment-level.
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Ensure that you have admin privileges to the repository you want to sync secrets to.
<Tabs> <Tabs>
<Tab title="Usage"> <Tab title="Usage">
<Warning>
Infisical can sync secrets to GitHub repo secrets only. If your repo uses environment secrets, then stay tuned with this [issue](https://github.com/Infisical/infisical/issues/54).
</Warning>
Prerequisites:
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
- Ensure you have admin privileges to the repo you want to sync secrets to.
<Steps> <Steps>
<Step title="Authorize Infisical for GitHub"> <Step title="Authorize Infisical for GitHub">
Navigate to your project's integrations tab in Infisical. Navigate to your project's integrations tab in Infisical.
@@ -29,12 +26,27 @@ description: "How to sync secrets from Infisical to GitHub Actions"
Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform. Although this step breaks E2EE, it's necessary for Infisical to sync the environment variables to the cloud platform.
</Info> </Info>
</Step> </Step>
<Step title="Start integration"> <Step title="Configure Infisical GitHub integration">
Select which Infisical environment secrets you want to sync to which GitHub repo and press start integration to start syncing secrets to the repo. Select which Infisical environment secrets you want to sync to which GitHub organization, repository, or repository environment.
<Tabs>
<Tab title="Repository">
![integrations github](../../images/integrations/github/integrations-github-scope-repo.png)
</Tab>
<Tab title="Organization">
![integrations github](../../images/integrations/github/integrations-github-scope-org.png)
</Tab>
<Tab title="Repository Environment">
![integrations github](../../images/integrations/github/integrations-github-scope-env.png)
</Tab>
</Tabs>
Finally, press create integration to start syncing secrets to GitHub.
![integrations github](../../images/integrations/github/integrations-github.png) ![integrations github](../../images/integrations/github/integrations-github.png)
</Step> </Step>
</Steps> </Steps>
</Tab> </Tab>
<Tab title="Self-Hosted Setup"> <Tab title="Self-Hosted Setup">
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
@@ -70,6 +82,6 @@ description: "How to sync secrets from Infisical to GitHub Actions"
Once added, restart your Infisical instance and use the GitHub integration. Once added, restart your Infisical instance and use the GitHub integration.
</Step> </Step>
</Steps> </Steps>
</Tab> </Tab>
</Tabs> </Tabs>
@@ -6,6 +6,8 @@ export {
useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthBitBucketWorkspaces,
useGetIntegrationAuthById, useGetIntegrationAuthById,
useGetIntegrationAuthChecklyGroups, useGetIntegrationAuthChecklyGroups,
useGetIntegrationAuthGithubEnvs,
useGetIntegrationAuthGithubOrgs,
useGetIntegrationAuthNorthflankSecretGroups, useGetIntegrationAuthNorthflankSecretGroups,
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthRailwayServices, useGetIntegrationAuthRailwayServices,
@@ -39,6 +39,10 @@ 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,
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: ({
@@ -177,6 +181,32 @@ 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 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 }
@@ -301,8 +331,6 @@ const fetchIntegrationAuthHerokuPipelines = async ({ integrationAuthId }: {
`/api/v1/integration-auth/${integrationAuthId}/heroku/pipelines` `/api/v1/integration-auth/${integrationAuthId}/heroku/pipelines`
); );
console.log(99999, pipelines)
return pipelines; return pipelines;
}; };
@@ -482,6 +510,30 @@ export const useGetIntegrationAuthChecklyGroups = ({
}); });
}; };
export const useGetIntegrationAuthGithubOrgs = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthGithubOrgs(integrationAuthId),
queryFn: () => fetchIntegrationAuthGithubOrgs(integrationAuthId),
enabled: true
});
};
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),
+13 -14
View File
@@ -11,22 +11,21 @@ export type TCloudIntegration = {
export type TIntegration = { export type TIntegration = {
id: string; id: string;
projectId: string;
envId: string;
environment: { slug: string; name: string; id: string };
isActive: boolean; isActive: boolean;
url: any; url?: string;
app: string; app?: string;
appId: string; appId?: string;
targetEnvironment: string; targetEnvironment?: string;
targetEnvironmentId: string; targetEnvironmentId?: string;
targetService: string; targetService?: string;
targetServiceId: string; targetServiceId?: string;
owner: string; owner?: string;
path: string; path?: string;
region: string; region?: string;
scope?: string;
integration: string; integration: string;
integrationAuth: string; integrationAuthId: string;
envId: string;
secretPath: string; secretPath: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
+489 -201
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
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";
import Link from "next/link"; import Link from "next/link";
@@ -12,11 +13,14 @@ import {
faCircleInfo faCircleInfo
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import axios from "axios";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import queryString from "query-string"; import queryString from "query-string";
import { twMerge } from "tailwind-merge";
import * as yup from "yup";
import { useCreateIntegration } from "@app/hooks/api"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
Button, Button,
Card, Card,
@@ -33,263 +37,547 @@ 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"; useGetIntegrationAuthGithubEnvs,
import { useGetWorkspaceById } from "../../../hooks/api/workspace"; useGetIntegrationAuthGithubOrgs,
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({
selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"),
secretPath: yup.string().trim().required("Secrets Path is required"),
secretSuffix: yup.string().trim().optional(),
scope: yup.mixed<TargetEnv>().oneOf(targetEnv.slice()).required(),
repoIds: yup.mixed().when("scope", {
is: "github-repo",
then: yup.array(yup.string().required()).min(1, "Select at least one repositories")
}),
repoId: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Repository is required")
}),
repoName: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Repository is required")
}),
repoOwner: yup.mixed().when("scope", {
is: "github-env",
then: yup.string().required("Repository is required")
}),
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>;
export default function GitHubCreateIntegrationPage() { export default function GitHubCreateIntegrationPage() {
const router = useRouter(); const router = useRouter();
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
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 [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); const { data: integrationAuthOrgs } = useGetIntegrationAuthGithubOrgs(
const [secretPath, setSecretPath] = useState("/"); integrationAuthId as string
const [targetAppIds, setTargetAppIds] = useState<string[]>([]); );
const [secretSuffix, setSecretSuffix] = useState("");
const { control, handleSubmit, watch, setValue } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
secretPath: "/",
scope: "github-repo",
repoIds: []
}
});
const scope = watch("scope");
const repoId = watch("repoId");
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(() => {
if (workspace) { if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug); setValue("selectedSourceEnvironment", workspace.environments[0].slug);
} }
}, [workspace]); }, [workspace]);
useEffect(() => { useEffect(() => {
if (integrationAuthApps) { if (integrationAuthGithubEnvs && integrationAuthGithubEnvs?.length > 0) {
if (integrationAuthApps.length > 0) { setValue("envId", integrationAuthGithubEnvs[0].envId);
setTargetAppIds([String(integrationAuthApps[0].appId)]); } else {
} else { setValue("envId", undefined);
setTargetAppIds(["none"]);
}
} }
}, [integrationAuthApps]); }, [integrationAuthGithubEnvs]);
const handleButtonClick = async () => { 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) {
targetAppIds.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, secretPath: data.secretPath,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: data.selectedSourceEnvironment,
owner: targetApp.owner, scope: data.scope,
secretPath, owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name,
metadata: { metadata: {
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,
appId: data.repoId,
owner: repoOwner,
targetEnvironmentId: data.envId,
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)) {
const { message } = err?.response?.data as { message: string };
errorMessage = message;
}
createNotification({
text: errorMessage,
type: "error"
});
setIsLoading(false);
} }
}; };
return integrationAuth && return integrationAuth && workspace && integrationAuthApps ? (
workspace && <div className="flex h-full w-full flex-col items-center justify-center py-4">
selectedSourceEnvironment &&
integrationAuthApps &&
targetAppIds ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<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" />
</Head> </Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600 p-0"> <Card className="max-w-lg rounded-md border border-mineshaft-600 p-0">
<CardTitle <form onSubmit={handleSubmit(onFormSubmit)} className="px-6">
className="px-6 text-left text-xl" <CardTitle
subTitle="Choose which environment in Infisical you want to sync to environment variables in GitHub." className="px-0 text-left text-xl"
> subTitle="Choose which environment in Infisical you want to sync to environment variables in GitHub."
<div className="flex flex-row items-center"> >
<div className="inline flex items-center rounded-full bg-mineshaft-200"> <div className="flex flex-row items-center">
<Image <div className="flex items-center rounded-full bg-mineshaft-200">
src="/images/integrations/GitHub.png" <Image
height={30} src="/images/integrations/GitHub.png"
width={30} height={30}
alt="GitHub logo" width={30}
/> alt="GitHub logo"
/>
</div>
<span className="ml-2.5">GitHub Integration </span>
<Link href="https://infisical.com/docs/integrations/cicd/githubactions" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div> </div>
<span className="ml-2.5">GitHub Integration </span> </CardTitle>
<Link href="https://infisical.com/docs/integrations/cicd/githubactions" passHref> <Tabs defaultValue={TabSections.Connection}>
<a target="_blank" rel="noopener noreferrer"> <TabList>
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100"> <div className="flex w-full flex-row border-b border-mineshaft-600">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" /> <Tab value={TabSections.Connection}>Connection</Tab>
Docs <Tab value={TabSections.Options}>Options</Tab>
<FontAwesomeIcon </div>
icon={faArrowUpRightFromSquare} </TabList>
className="ml-1.5 mb-[0.07rem] text-xxs" <TabPanel value={TabSections.Connection}>
/> <motion.div
</div> key="panel-1"
</a> transition={{ duration: 0.15 }}
</Link> initial={{ opacity: 0, translateX: 30 }}
</div> animate={{ opacity: 1, translateX: 0 }}
</CardTitle> exit={{ opacity: 0, translateX: 30 }}
<Tabs defaultValue={TabSections.Connection} className="px-6"> >
<TabList> <Controller
<div className="flex w-full flex-row border-b border-mineshaft-600"> control={control}
<Tab value={TabSections.Connection}>Connection</Tab> name="selectedSourceEnvironment"
<Tab value={TabSections.Options}>Options</Tab> render={({ field: { onChange, ...field }, fieldState: { error } }) => (
</div> <FormControl
</TabList> label="Project Environment"
<TabPanel value={TabSections.Connection}> errorText={error?.message}
<motion.div isError={Boolean(error)}
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Project Environment">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
> >
{sourceEnvironment.name} <Select
</SelectItem> defaultValue={field.value}
))} onValueChange={onChange}
</Select> className="w-full border border-mineshaft-500"
</FormControl> >
<FormControl label="Secrets Path"> {workspace?.environments.map((sourceEnvironment) => (
<Input <SelectItem
value={secretPath} value={sourceEnvironment.slug}
onChange={(evt) => setSecretPath(evt.target.value)} key={`source-environment-${sourceEnvironment.slug}`}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="GitHub Repo">
<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">
{targetAppIds.length === 1
? integrationAuthApps?.find(
(integrationAuthApp) =>
targetAppIds[0] === String(integrationAuthApp.appId)
)?.name
: `${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))) {
setTargetAppIds(
targetAppIds.filter(
(appId) => appId !== String(integrationAuthApp.appId)
)
);
} else {
setTargetAppIds([
...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} {sourceEnvironment.name}
</DropdownMenuItem> </SelectItem>
); ))}
}) </Select>
) : ( </FormControl>
<div /> )}
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<FormControl label="Append Secret Names with..." className="pb-[9.75rem]">
<Input
value={secretSuffix}
onChange={(evt) => setSecretSuffix(evt.target.value)}
placeholder="Provide a suffix for secret names, default is no suffix"
/> />
</FormControl> <Controller
</motion.div> control={control}
</TabPanel> name="secretPath"
</Tabs> render={({ field, fieldState: { error } }) => (
<Button <FormControl
onClick={handleButtonClick} label="Secrets Path"
color="mineshaft" errorText={error?.message}
variant="outline_bg" isError={Boolean(error)}
className="mb-6 ml-auto mr-6" >
isLoading={isLoading} <Input {...field} placeholder="Provide a path, default is /" />
isDisabled={integrationAuthApps.length === 0 || targetAppIds.length === 0} </FormControl>
> )}
Create Integration />
</Button> <Controller
control={control}
name="scope"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Scope" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
>
<SelectItem value="github-org">Organization</SelectItem>
<SelectItem value="github-repo">Repository</SelectItem>
<SelectItem value="github-env">Repository Environment</SelectItem>
</Select>
</FormControl>
)}
/>
{scope === "github-repo" && (
<Controller
control={control}
name="repoIds"
render={({ field: { onChange }, fieldState: { error } }) => (
<FormControl
label="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"
>
{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: 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-org" && (
<Controller
control={control}
name="orgId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Organization"
errorText={
integrationAuthOrgs?.length ? error?.message : "No organizations found"
}
isError={Boolean(integrationAuthOrgs?.length && error?.message)}
>
<Select
value={field.value}
onValueChange={onChange}
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="Repository"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
value={field.value}
onValueChange={(e) => {
const selectedRepo = integrationAuthApps.find((app) => app.appId === e);
setValue("repoName", selectedRepo?.name);
setValue("repoOwner", selectedRepo?.owner);
onChange(e);
}}
className="w-full border border-mineshaft-500"
>
{integrationAuthApps?.length ? (
integrationAuthApps.map((app) => {
return (
<SelectItem
value={app.appId as string}
key={`repo-id-${app.appId}`}
className="w-[28.4rem] text-sm"
>
{app.owner}/{app.name}
</SelectItem>
);
})
) : (
<div />
)}
</Select>
</FormControl>
)}
/>
)}
{scope === "github-env" && (
<Controller
control={control}
name="envId"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Environment"
errorText={
integrationAuthGithubEnvs?.length
? error?.message
: "No Environment found"
}
isError={Boolean(integrationAuthGithubEnvs?.length || error?.message)}
>
<Select
value={field.value}
onValueChange={onChange}
isDisabled={!repoId}
className={twMerge(
"w-full border border-mineshaft-500",
!repoId && "h-10 cursor-not-allowed"
)}
>
{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>
</FormControl>
)}
/>
)}
</motion.div>
</TabPanel>
<TabPanel value={TabSections.Options}>
<motion.div
key="panel-1"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: -30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<Controller
control={control}
name="secretSuffix"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Append Secret Names with..."
className="pb-[9.75rem]"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="Provide a suffix for secret names, default is no suffix"
/>
</FormControl>
)}
/>
</motion.div>
</TabPanel>
</Tabs>
<div className="flex w-full justify-end">
<Button
type="submit"
color="mineshaft"
variant="outline_bg"
className="mb-6"
isLoading={isLoading}
>
Create Integration
</Button>
</div>
</form>
</Card> </Card>
<div className="mt-6 w-full max-w-md border-t border-mineshaft-800" /> <div className="mt-6 w-full max-w-md border-t border-mineshaft-800" />
<div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"> <div className="mt-6 flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4">
@@ -318,7 +606,7 @@ export default function GitHubCreateIntegrationPage() {
/> />
) : ( ) : (
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200"> <div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" /> <FontAwesomeIcon icon={faBugs} className="li my-2 inline text-6xl" />
<p> <p>
Something went wrong. Please contact{" "} Something went wrong. Please contact{" "}
<a <a
@@ -60,7 +60,7 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`; link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break; break;
case "github": case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`; link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo,admin:org&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break; break;
case "gitlab": case "gitlab":
link = `${window.location.origin}/integrations/gitlab/authorize`; link = `${window.location.origin}/integrations/gitlab/authorize`;
@@ -9,11 +9,8 @@ import {
AlertDescription, AlertDescription,
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
FormControl,
FormLabel, FormLabel,
IconButton, IconButton,
Select,
SelectItem,
Skeleton, Skeleton,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
@@ -22,7 +19,7 @@ import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types"; import { TIntegration } from "@app/hooks/api/types";
type Props = { type Props = {
environments: Array<{ name: string; slug: string }>; environments: Array<{ name: string; slug: string; id: string }>;
integrations?: TIntegration[]; integrations?: TIntegration[];
isLoading?: boolean; isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void; onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
@@ -80,29 +77,15 @@ export const IntegrationsSection = ({
<div className="flex flex-col space-y-4 p-6 pt-0"> <div className="flex flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => ( {integrations?.map((integration) => (
<div <div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3 pb-0" className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
key={`integration-${integration?.id.toString()}`} key={`integration-${integration?.id.toString()}`}
> >
<div className="flex"> <div className="flex">
<div> <div className="ml-2 flex flex-col">
<FormControl label="Environment"> <FormLabel label="Environment" />
<Select <div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
value={integration.environment.slug} {environments.find((e) => e.id === integration.envId)?.name || "-"}
isDisabled={integration.isActive} </div>
className="min-w-[8rem] border border-mineshaft-700"
>
{environments.map((environment) => {
return (
<SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name}
</SelectItem>
);
})}
</Select>
</FormControl>
</div> </div>
<div className="ml-2 flex flex-col"> <div className="ml-2 flex flex-col">
<FormLabel label="Secret Path" /> <FormLabel label="Secret Path" />
@@ -142,11 +125,22 @@ export const IntegrationsSection = ({
</div> </div>
)} )}
<div className="ml-2 flex flex-col"> <div className="ml-2 flex flex-col">
<FormLabel label={integration?.metadata?.scope || "App"} /> <FormLabel
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200"> label={
{integration.integration === "hashicorp-vault" (integration.integration === "qovery" && integration?.scope) ||
? `${integration.app} - path: ${integration.path}` (integration?.scope === "github-org" && "Organization") ||
: integration.app} (["github-repo", "github-env"].includes(integration?.scope as string) &&
"Repository") ||
"App"
}
/>
<div className="min-w-[8rem] max-w-[12rem] overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) ||
(integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) ||
integration.app}
</div> </div>
</div> </div>
{(integration.integration === "vercel" || {(integration.integration === "vercel" ||
@@ -154,32 +148,31 @@ export const IntegrationsSection = ({
integration.integration === "railway" || integration.integration === "railway" ||
integration.integration === "gitlab" || integration.integration === "gitlab" ||
integration.integration === "teamcity" || integration.integration === "teamcity" ||
integration.integration === "bitbucket") && ( integration.integration === "bitbucket" ||
(integration.integration === "github" && integration.scope === "github-env")) && (
<div className="ml-4 flex flex-col"> <div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" /> <FormLabel label="Target Environment" />
<div className="overflow-clip text-ellipsis whitespace-nowrap rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment || integration.targetEnvironmentId}
</div>
</div>
)}
{integration.integration === "checkly" && integration.targetService && (
<div className="ml-2">
<FormLabel label="Group" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200"> <div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment} {integration.targetService}
</div> </div>
</div> </div>
)} )}
{(integration.integration === "checkly" || {(integration.integration === "checkly" ||
integration.integration === "github") && ( integration.integration === "github") && (
<> <div className="ml-2">
{integration.targetService && ( <FormLabel label="Secret Suffix" />
<div className="ml-2"> <div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
<FormLabel label="Group" /> {integration?.metadata?.secretSuffix || "-"}
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetService}
</div>
</div>
)}
<div className="ml-2">
<FormLabel label="Secret Suffix" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration?.metadata?.secretSuffix || "-"}
</div>
</div> </div>
</> </div>
)} )}
</div> </div>
<div className="flex cursor-default items-center"> <div className="flex cursor-default items-center">
@@ -214,7 +207,7 @@ export const IntegrationsSection = ({
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " " (popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`} } integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || ""} deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || (popUp?.deleteConfirmation?.data as TIntegration)?.owner || ""}
onDeleteApproved={async () => onDeleteApproved={async () =>
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () => onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
handlePopUpClose("deleteConfirmation") handlePopUpClose("deleteConfirmation")