diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index bfa559c74..331dadee1 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -991,7 +991,7 @@ const syncSecretsCircleci = async ({ integration: IIntegration; secrets: any; accessToken: string; -}) => { +}) => { try { const circleciOrganizationDetail = ( await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { @@ -1004,19 +1004,6 @@ const syncSecretsCircleci = async ({ const { slug } = circleciOrganizationDetail; - // get secrets from CircleCI - const getSecretsRes = ( - await axios.get( - `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, - { - headers: { - "Circle-Token": accessToken, - "Accept-Encoding": "application/json", - }, - } - ) - ).data?.items; - // inject secrets to CircleCI (one by one) Object.keys(secrets).forEach( async (key) => @@ -1035,6 +1022,20 @@ const syncSecretsCircleci = async ({ ) ); + // get secrets from CircleCI + const getSecretsRes = ( + await axios.get( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + } + ) + ).data?.items; + + // delete secrets from CircleCI getSecretsRes.forEach(async (sec: any) => { if (!(sec.name in secrets)) { await axios.delete( @@ -1042,6 +1043,7 @@ const syncSecretsCircleci = async ({ { headers: { "Circle-Token": accessToken, + "Content-Type": "application/json", }, } ); diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx index d802e81a8..45314abfd 100644 --- a/frontend/src/pages/integrations/[id].tsx +++ b/frontend/src/pages/integrations/[id].tsx @@ -192,6 +192,9 @@ export default function Integrations() { case 'flyio': link = `${window.location.origin}/integrations/flyio/authorize` break; + case 'circleci': + link = `${window.location.origin}/integrations/circleci/authorize` + break; default: break; } diff --git a/frontend/src/pages/integrations/circleci/authorize.tsx b/frontend/src/pages/integrations/circleci/authorize.tsx new file mode 100644 index 000000000..3033a7d45 --- /dev/null +++ b/frontend/src/pages/integrations/circleci/authorize.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; + +import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps'; +import { + Button, + Card, + CardTitle, + FormControl, + Input, +} from '../../../components/v2'; +import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken"; + +export default function CircleCICreateIntegrationPage() { + const router = useRouter(); + 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 saveIntegrationAccessToken({ + workspaceId: localStorage.getItem('projectData.id'), + integration: 'circleci', + accessToken: apiKey + }); + + setIsLoading(false); + + router.push( + `/integrations/circleci/create?integrationAuthId=${integrationAuth._id}` + ); + } catch (err) { + console.error(err); + } + } + + return ( +
+ + CircleCI Integration + + setApiKey(e.target.value)} + /> + + + +
+ ) +} + +CircleCICreateIntegrationPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file diff --git a/frontend/src/pages/integrations/circleci/create.tsx b/frontend/src/pages/integrations/circleci/create.tsx new file mode 100644 index 000000000..2dae88654 --- /dev/null +++ b/frontend/src/pages/integrations/circleci/create.tsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/router'; +import queryString from 'query-string'; + +import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps'; +import { + Button, + Card, + CardTitle, + FormControl, + Select, + SelectItem +} from '../../../components/v2'; +import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth'; +import { useGetWorkspaceById } from '../../../hooks/api/workspace'; +import createIntegration from "../../api/integrations/createIntegration"; + +export default function CircleCICreateIntegrationPage() { + const router = useRouter(); + + const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]); + + const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? ''); + const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? ''); + const { data: integrationAuthApps } = useGetIntegrationAuthApps(integrationAuthId as string ?? ''); + + const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(''); + const [targetApp, setTargetApp] = useState(''); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (workspace) { + setSelectedSourceEnvironment(workspace.environments[0].slug); + } + }, [workspace]); + + useEffect(() => { + // TODO: handle case where apps can be empty + if (integrationAuthApps) { + setTargetApp(integrationAuthApps[0].name); + } + }, [integrationAuthApps]); + + const handleButtonClick = async () => { + try { + if (!integrationAuth?._id) return; + + setIsLoading(true); + + await createIntegration({ + integrationAuthId: integrationAuth?._id, + isActive: true, + app: targetApp, + appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null, + sourceEnvironment: selectedSourceEnvironment, + targetEnvironment: null, + owner: null + }); + + setIsLoading(false); + + router.push( + `/integrations/${localStorage.getItem('projectData.id')}` + ); + } catch (err) { + console.error(err); + } + } + + return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? ( +
+ + CircleCI Integration + + + + + + + + +
+ ) :
+} + +CircleCICreateIntegrationPage.requireAuth = true; + +export const getServerSideProps = getTranslatedServerSideProps(['integrations']); \ No newline at end of file