From cc3d132f5d355ab1e1987421e3f9bc277ec76b95 Mon Sep 17 00:00:00 2001 From: McPizza Date: Tue, 10 Dec 2024 20:07:23 +0100 Subject: [PATCH] feat(integrations): New CircleCI Context Sync --- .../routes/v1/integration-auth-router.ts | 34 +++ .../integration-auth/integration-app-list.ts | 48 ++++ .../integration-auth/integration-app-types.ts | 5 + .../integration-auth-service.ts | 37 +++ .../integration-auth-types.ts | 12 + .../integration-auth/integration-list.ts | 12 + .../integration-sync-secret.ts | 90 +++++++ frontend/public/data/frequentConstants.ts | 1 + .../src/hooks/api/integrationAuth/index.tsx | 1 + .../src/hooks/api/integrationAuth/queries.tsx | 21 +- .../src/hooks/api/integrationAuth/types.ts | 5 + .../circleci-context/authorize.tsx | 101 ++++++++ .../integrations/circleci-context/create.tsx | 245 ++++++++++++++++++ .../IntegrationConnectionSection.tsx | 12 +- .../IntegrationPage.utils.tsx | 3 + .../components/IntegrationDetails.tsx | 8 + 16 files changed, 633 insertions(+), 2 deletions(-) create mode 100644 backend/src/services/integration-auth/integration-app-types.ts create mode 100644 frontend/src/pages/integrations/circleci-context/authorize.tsx create mode 100644 frontend/src/pages/integrations/circleci-context/create.tsx diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 575544cc7..36589c500 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -1123,4 +1123,38 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) return { spaces }; } }); + + server.route({ + method: "GET", + url: "/:integrationAuthId/circleci/organizations", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT]), + schema: { + params: z.object({ + integrationAuthId: z.string().trim() + }), + response: { + 200: z.object({ + organizations: z + .object({ + name: z.string(), + slug: z.string() + }) + .array() + }) + } + }, + handler: async (req) => { + const organizations = await server.services.integrationAuth.getCircleCIOrganizations({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + id: req.params.integrationAuthId + }); + return { organizations }; + } + }); }; diff --git a/backend/src/services/integration-auth/integration-app-list.ts b/backend/src/services/integration-auth/integration-app-list.ts index 3b8078cc1..2dfa65890 100644 --- a/backend/src/services/integration-auth/integration-app-list.ts +++ b/backend/src/services/integration-auth/integration-app-list.ts @@ -8,6 +8,7 @@ import { getConfig } from "@app/lib/config/env"; import { request } from "@app/lib/config/request"; import { NotFoundError } from "@app/lib/errors"; +import { TCircleCIContext } from "./integration-app-types"; import { IntegrationAuthMetadataSchema, TIntegrationAuthMetadata } from "./integration-auth-schema"; import { Integrations, IntegrationUrls } from "./integration-list"; @@ -489,6 +490,47 @@ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { return apps; }; +/** + * Return list of contexts for CircleCI_Context integration + */ +const getAppsCircleCIContexts = async ({ accessToken, orgSlug }: { accessToken: string; orgSlug: string }) => { + type NextPageToken = string | null | undefined; + + type CircleCIContextResponse = { + items: TCircleCIContext[]; + next_page_token: NextPageToken; + }; + + const contexts: TCircleCIContext[] = []; + + let nextPageToken: NextPageToken; + + while (nextPageToken !== null) { + const res = ( + await request.get(`${IntegrationUrls.CIRCLECI_CONTEXT_API_URL}/v2/context`, { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json" + }, + params: new URLSearchParams({ + "owner-slug": orgSlug, + ...(nextPageToken ? { "page-token": nextPageToken } : {}) + }) + }) + ).data; + + contexts.push(...res.items); + nextPageToken = res.next_page_token; + } + + const apps = contexts?.map((context) => ({ + name: context.name, + appId: context.id + })); + + return apps; +}; + /** * Return list of projects for Databricks integration */ @@ -1195,6 +1237,12 @@ export const getApps = async ({ accessToken }); + case Integrations.CIRCLECI_CONTEXT: + return getAppsCircleCIContexts({ + accessToken, + orgSlug: workspaceSlug as string + }); + case Integrations.DATABRICKS: return getAppsDatabricks({ url, diff --git a/backend/src/services/integration-auth/integration-app-types.ts b/backend/src/services/integration-auth/integration-app-types.ts new file mode 100644 index 000000000..1ddd2e4d2 --- /dev/null +++ b/backend/src/services/integration-auth/integration-app-types.ts @@ -0,0 +1,5 @@ +export type TCircleCIContext = { + id: string; + name: string; + created_at: string; +}; diff --git a/backend/src/services/integration-auth/integration-auth-service.ts b/backend/src/services/integration-auth/integration-auth-service.ts index be1a8d53c..cbbffa431 100644 --- a/backend/src/services/integration-auth/integration-auth-service.ts +++ b/backend/src/services/integration-auth/integration-auth-service.ts @@ -25,6 +25,7 @@ import { TBitbucketEnvironment, TBitbucketWorkspace, TChecklyGroups, + TCircleCIOrganization, TDeleteIntegrationAuthByIdDTO, TDeleteIntegrationAuthsDTO, TDuplicateGithubIntegrationAuthDTO, @@ -36,6 +37,7 @@ import { TIntegrationAuthBitbucketEnvironmentsDTO, TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthChecklyGroupsDTO, + TIntegrationAuthCircleCIOrganizationDTO, TIntegrationAuthGithubEnvsDTO, TIntegrationAuthGithubOrgsDTO, TIntegrationAuthHerokuPipelinesDTO, @@ -1427,6 +1429,40 @@ export const integrationAuthServiceFactory = ({ return []; }; + const getCircleCIOrganizations = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + id + }: TIntegrationAuthCircleCIOrganizationDTO) => { + const integrationAuth = await integrationAuthDAL.findById(id); + if (!integrationAuth) throw new NotFoundError({ message: `Integration auth with ID '${id}' not found` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + integrationAuth.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); + const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(integrationAuth.projectId); + const { accessToken } = await getIntegrationAccessToken(integrationAuth, shouldUseSecretV2Bridge, botKey); + + const { data }: { data: TCircleCIOrganization[] } = await request.get( + `${IntegrationUrls.CIRCLECI_CONTEXT_API_URL}/v2/me/collaborations`, + { + headers: { + "Circle-Token": `${accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + return data; + }; + const deleteIntegrationAuths = async ({ projectId, integration, @@ -1638,6 +1674,7 @@ export const integrationAuthServiceFactory = ({ getTeamcityBuildConfigs, getBitbucketWorkspaces, getBitbucketEnvironments, + getCircleCIOrganizations, getIntegrationAccessToken, duplicateIntegrationAuth, getOctopusDeploySpaces, diff --git a/backend/src/services/integration-auth/integration-auth-types.ts b/backend/src/services/integration-auth/integration-auth-types.ts index 80e8d6c36..bd977333a 100644 --- a/backend/src/services/integration-auth/integration-auth-types.ts +++ b/backend/src/services/integration-auth/integration-auth-types.ts @@ -123,6 +123,10 @@ export type TGetIntegrationAuthTeamCityBuildConfigDTO = { appId: string; } & Omit; +export type TIntegrationAuthCircleCIOrganizationDTO = { + id: string; +} & Omit; + export type TVercelBranches = { ref: string; lastCommit: string; @@ -184,6 +188,14 @@ export type TTeamCityBuildConfig = { webUrl: string; }; +export type TCircleCIOrganization = { + id: string; + vcsType: string; + name: string; + avatarUrl: string; + slug: string; +}; + export type TIntegrationsWithEnvironment = TIntegrations & { environment?: | { diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index 45cbdaea9..2c7e7e288 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -15,6 +15,7 @@ export enum Integrations { FLYIO = "flyio", LARAVELFORGE = "laravel-forge", CIRCLECI = "circleci", + CIRCLECI_CONTEXT = "circleci-context", DATABRICKS = "databricks", TRAVISCI = "travisci", TEAMCITY = "teamcity", @@ -76,6 +77,8 @@ export enum IntegrationUrls { RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2", FLYIO_API_URL = "https://api.fly.io/graphql", CIRCLECI_API_URL = "https://circleci.com/api", + // eslint-disable-next-line + CIRCLECI_CONTEXT_API_URL = "https://circleci.com/api", DATABRICKS_API_URL = "https:/xxxx.com/api", TRAVISCI_API_URL = "https://api.travis-ci.com", SUPABASE_API_URL = "https://api.supabase.com", @@ -226,6 +229,15 @@ export const getIntegrationOptions = async () => { clientId: "", docsLink: "" }, + { + name: "Circle CI Contexts", + slug: "circleci-context", + image: "Circle CI.png", + isAvailable: true, + type: "pat", + clientId: "", + docsLink: "" + }, { name: "Databricks", slug: "databricks", diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index c147150f0..ed917b936 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -2343,6 +2343,89 @@ const syncSecretsCircleCI = async ({ ); }; +/** + * Sync/push [secrets] to CircleCI Context + */ +const syncSecretsCircleCIContext = async ({ + integration, + secrets, + accessToken +}: { + integration: TIntegrations; + secrets: Record; + accessToken: string; +}) => { + // sync secrets to CircleCI + await Promise.all( + Object.keys(secrets).map(async (key) => + request.put( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${key}`, + { + value: secrets[key].value + }, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + } + ) + ) + ); + + // get secrets from CircleCI + const getSecretsRes = async () => { + type EnvVars = { + variable: string; + created_at: string; + updated_at: string; + context_id: string; + }; + + type ResponseSchema = { + items: EnvVars[]; + next_page_token: string | null; + }; + + let nextPageToken: string | null | undefined; + const envVars: EnvVars[] = []; + + while (nextPageToken !== null) { + const res = await request.get( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json" + } + } + ); + + envVars.push(...res.data.items); + nextPageToken = res.data.next_page_token; + } + + return envVars; + }; + + // delete secrets from CircleCI + await Promise.all( + (await getSecretsRes()).map(async (sec) => { + if (!(sec.variable in secrets)) { + return request.delete( + `${IntegrationUrls.CIRCLECI_API_URL}/v2/context/${integration.appId}/environment-variable/${sec.variable}`, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json" + } + } + ); + } + }) + ); +}; + /** * Sync/push [secrets] to Databricks project */ @@ -4433,6 +4516,13 @@ export const syncIntegrationSecrets = async ({ accessToken }); break; + case Integrations.CIRCLECI_CONTEXT: + await syncSecretsCircleCIContext({ + integration, + secrets, + accessToken + }); + break; case Integrations.DATABRICKS: await syncSecretsDatabricks({ integration, diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 51c006424..5d496db75 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -16,6 +16,7 @@ const integrationSlugNameMapping: Mapping = { railway: "Railway", flyio: "Fly.io", circleci: "CircleCI", + "circleci-context": "CircleCI Context", databricks: "Databricks", travisci: "TravisCI", supabase: "Supabase", diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx index 0ae3511de..e7ee5928a 100644 --- a/frontend/src/hooks/api/integrationAuth/index.tsx +++ b/frontend/src/hooks/api/integrationAuth/index.tsx @@ -7,6 +7,7 @@ export { useGetIntegrationAuthBitBucketWorkspaces, useGetIntegrationAuthById, useGetIntegrationAuthChecklyGroups, + useGetIntegrationAuthCircleCIOrganizations, useGetIntegrationAuthGithubEnvs, useGetIntegrationAuthGithubOrgs, useGetIntegrationAuthNorthflankSecretGroups, diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx index e5f928158..84a50ae1f 100644 --- a/frontend/src/hooks/api/integrationAuth/queries.tsx +++ b/frontend/src/hooks/api/integrationAuth/queries.tsx @@ -8,6 +8,7 @@ import { BitBucketEnvironment, BitBucketWorkspace, ChecklyGroup, + CircleCIOrganization, Environment, HerokuPipelineCoupling, IntegrationAuth, @@ -128,7 +129,9 @@ const integrationAuthKeys = { integrationAuthId, ...params }: TGetIntegrationAuthOctopusDeployScopeValuesDTO) => - [{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const + [{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const, + getIntegrationAuthCircleCIOrganizations: (integrationAuthId: string) => + [{ integrationAuthId }, "getIntegrationAuthCircleCIOrganizations"] as const }; const fetchIntegrationAuthById = async (integrationAuthId: string) => { @@ -510,6 +513,15 @@ const fetchIntegrationAuthOctopusDeployScopeValues = async ({ return data; }; +const fetchIntegrationAuthCircleCIOrganizations = async (integrationAuthId: string) => { + const { + data: { organizations } + } = await apiRequest.get<{ + organizations: CircleCIOrganization[]; + }>(`/api/v1/integration-auth/${integrationAuthId}/circleci/organizations`); + return organizations; +}; + export const useGetIntegrationAuthById = (integrationAuthId: string) => { return useQuery({ queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), @@ -884,6 +896,13 @@ export const useGetIntegrationAuthTeamCityBuildConfigs = ({ }); }; +export const useGetIntegrationAuthCircleCIOrganizations = (integrationAuthId: string) => { + return useQuery({ + queryKey: integrationAuthKeys.getIntegrationAuthCircleCIOrganizations(integrationAuthId), + queryFn: () => fetchIntegrationAuthCircleCIOrganizations(integrationAuthId) + }); +}; + export const useAuthorizeIntegration = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts index 58a643dff..7a1d469b9 100644 --- a/frontend/src/hooks/api/integrationAuth/types.ts +++ b/frontend/src/hooks/api/integrationAuth/types.ts @@ -105,6 +105,11 @@ export enum OctopusDeployScope { // tenant, variable set } +export type CircleCIOrganization = { + name: string; + slug: string; +}; + export type TGetIntegrationAuthOctopusDeployScopeValuesDTO = { integrationAuthId: string; spaceId: string; diff --git a/frontend/src/pages/integrations/circleci-context/authorize.tsx b/frontend/src/pages/integrations/circleci-context/authorize.tsx new file mode 100644 index 000000000..67252c240 --- /dev/null +++ b/frontend/src/pages/integrations/circleci-context/authorize.tsx @@ -0,0 +1,101 @@ +import { useState } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2"; +import { useSaveIntegrationAccessToken } from "@app/hooks/api"; + +export default function CircleCIContextCreateIntegrationPage() { + const router = useRouter(); + const { mutateAsync } = useSaveIntegrationAccessToken(); + + const [apiKey, setApiKey] = useState(""); + const [apiKeyErrorText, setApiKeyErrorText] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const handleButtonClick = async () => { + try { + setApiKeyErrorText(""); + if (apiKey.length === 0) { + setApiKeyErrorText("API Key cannot be blank"); + return; + } + + setIsLoading(true); + + const integrationAuth = await mutateAsync({ + workspaceId: localStorage.getItem("projectData.id"), + integration: "circleci-context", + accessToken: apiKey + }); + + setIsLoading(false); + + router.push(`/integrations/circleci-context/create?integrationAuthId=${integrationAuth.id}`); + } catch (err) { + console.error(err); + } + }; + + return ( +
+ + Authorize CircleCI Context Integration + + + + +
+
+ CircleCI logo +
+ CircleCI Context Integration + + +
+ + Docs + +
+
+ +
+
+ + setApiKey(e.target.value)} /> + + +
+
+ ); +} + +CircleCIContextCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/pages/integrations/circleci-context/create.tsx b/frontend/src/pages/integrations/circleci-context/create.tsx new file mode 100644 index 000000000..820490e32 --- /dev/null +++ b/frontend/src/pages/integrations/circleci-context/create.tsx @@ -0,0 +1,245 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + Card, + CardTitle, + FilterableSelect, + FormControl, + Input, + Spinner +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateIntegration } from "@app/hooks/api"; +import { + useGetIntegrationAuthApps, + useGetIntegrationAuthCircleCIOrganizations +} from "@app/hooks/api/integrationAuth"; + +const formSchema = z.object({ + secretPath: z.string().default("/"), + sourceEnvironment: z.object({ name: z.string(), slug: z.string() }), + targetOrg: z.object({ name: z.string(), slug: z.string() }), + targetContext: z.object({ name: z.string(), appId: z.string() }) +}); + +type TFormData = z.infer; + +export default function CircleCIContextCreateIntegrationPage() { + const router = useRouter(); + const { mutateAsync, isLoading: isCreatingIntegration } = useCreateIntegration(); + const { currentWorkspace, isLoading: isProjectLoading } = useWorkspace(); + + const integrationAuthId = router.query.integrationAuthId as string; + + const { watch, control, reset, handleSubmit } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + secretPath: "/", + sourceEnvironment: currentWorkspace?.environments[0] + } + }); + + const circleCiOrg = watch("targetOrg"); + + const { data: circleCIOrganizations, isLoading: isCircleCIOrganizationsLoading } = + useGetIntegrationAuthCircleCIOrganizations(integrationAuthId); + + const { data: circleCIContexts } = useGetIntegrationAuthApps( + { + integrationAuthId, + workspaceSlug: circleCiOrg?.slug + }, + + { enabled: Boolean(circleCiOrg?.slug) } + ); + + const onSubmit = async ({ + sourceEnvironment, + secretPath, + targetOrg, + targetContext + }: TFormData) => { + try { + await mutateAsync({ + integrationAuthId, + isActive: true, + sourceEnvironment: sourceEnvironment.slug, + app: targetContext.name, + appId: targetContext.appId, + owner: targetOrg.slug, + secretPath + }); + + createNotification({ + type: "success", + text: "Successfully created integration" + }); + router.push(`/integrations/${currentWorkspace?.id}`); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create integration" + }); + console.error(err); + } + }; + + useEffect(() => { + if (!circleCIContexts || !circleCIOrganizations || !currentWorkspace) return; + + reset({ + targetOrg: circleCIOrganizations[0], + targetContext: circleCIContexts[0] + }); + }, [circleCIOrganizations, circleCIContexts, currentWorkspace]); + + if (isProjectLoading || isCircleCIOrganizationsLoading) + return ( +
+ +
+ ); + + return ( +
+ + +
+
+ CircleCI logo + + CircleCI Context Integration +
+ + +
+ + Docs + +
+ +
+
+ ( + + option.slug} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={currentWorkspace?.environments} + placeholder="Select a project environment" + isDisabled={!currentWorkspace?.environments.length} + /> + + )} + /> + ( + + + + )} + /> + ( + + option.slug} + value={value} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={circleCIOrganizations} + placeholder={ + circleCIOrganizations?.length + ? "Select an organization..." + : "No organizations found..." + } + isDisabled={!circleCIOrganizations?.length} + /> + + )} + /> + ( + + option.appId!} + getOptionLabel={(option) => option.name} + onChange={onChange} + options={circleCIContexts} + placeholder={ + circleCIContexts?.length ? "Select a context..." : "No contexts found..." + } + isDisabled={!circleCIContexts?.length} + /> + + )} + /> + + +
+
+ ); +} + +CircleCIContextCreateIntegrationPage.requireAuth = true; diff --git a/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx b/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx index 778cdd00a..2bd7ee61d 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationDetailsPage/components/IntegrationConnectionSection.tsx @@ -46,6 +46,8 @@ export const IntegrationConnectionSection = ({ integration }: Props) => { case "qovery": return integration.scope; case "circleci": + case "circleci-context": + return "Context"; case "terraform-cloud": return "Project"; case "aws-secret-manager": @@ -77,7 +79,6 @@ export const IntegrationConnectionSection = ({ integration }: Props) => { return `${integration.owner}`; } return `${integration.owner}/${integration.app}`; - case "aws-parameter-store": case "rundeck": return `${integration.path}`; @@ -155,6 +156,15 @@ export const IntegrationConnectionSection = ({ integration }: Props) => { ); } + if (integration.integration === "circleci-context" && integration.owner) { + return ( +
+ +
{integration.owner}
+
+ ); + } + if (integration.integration === "terraform-cloud" && integration.targetService) { return (
diff --git a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx index e1a1ff6fb..298cb9372 100644 --- a/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx +++ b/frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx @@ -120,6 +120,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => case "circleci": link = `${window.location.origin}/integrations/circleci/authorize`; break; + case "circleci-context": + link = `${window.location.origin}/integrations/circleci-context/authorize`; + break; case "databricks": link = `${window.location.origin}/integrations/databricks/authorize`; break; diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationDetails.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationDetails.tsx index f785ca745..95b4c9d84 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationDetails.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/components/IntegrationDetails.tsx @@ -15,6 +15,7 @@ export const getIntegrationDestination = (integration: TIntegration) => (["aws-parameter-store", "rundeck"].includes(integration.integration) && `${integration.path}`) || (integration.scope?.startsWith("github-") && `${integration.owner}/${integration.app}`) || integration.app || + (integration.integration === "circleci-context" && `${integration.owner}`) || "-"; export const IntegrationDetails = ({ integration }: Props) => { @@ -53,6 +54,7 @@ export const IntegrationDetails = ({ integration }: Props) => { label={ (integration.integration === "qovery" && integration?.scope) || (integration.integration === "circleci" && "Project") || + (integration.integration === "circleci-context" && "Context") || (integration.integration === "bitbucket" && "Repository") || (integration.integration === "octopus-deploy" && "Project") || (integration.integration === "aws-secret-manager" && "Secret") || @@ -110,6 +112,12 @@ export const IntegrationDetails = ({ integration }: Props) => {
{integration.owner}
)} + {integration.integration === "circleci-context" && integration.owner && ( +
+ +
{integration.owner}
+
+ )} {integration.integration === "terraform-cloud" && integration.targetService && (