circle-ci integration on progress

This commit is contained in:
Aashish-Upadhyay-101
2023-02-07 13:20:39 +05:45
parent b0ffac2f00
commit 80d219c3e0
11 changed files with 379 additions and 335 deletions
@@ -1,23 +1,16 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import { import { Integration, IntegrationAuth, Bot } from "../../models";
Integration, import { INTEGRATION_SET, INTEGRATION_OPTIONS } from "../../variables";
IntegrationAuth, import { IntegrationService } from "../../services";
Bot import { getApps, revokeAccess } from "../../integrations";
} from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations';
export const getIntegrationOptions = async ( export const getIntegrationOptions = async (req: Request, res: Response) => {
req: Request, return res.status(200).send({
res: Response integrationOptions: INTEGRATION_OPTIONS,
) => { });
return res.status(200).send({ };
integrationOptions: INTEGRATION_OPTIONS
});
}
/** /**
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId] * Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
@@ -25,100 +18,103 @@ export const getIntegrationOptions = async (
* @param res * @param res
* @returns * @returns
*/ */
export const oAuthExchange = async ( export const oAuthExchange = async (req: Request, res: Response) => {
req: Request, try {
res: Response const { workspaceId, code, integration } = req.body;
) => {
try {
const { workspaceId, code, integration } = req.body;
if (!INTEGRATION_SET.has(integration)) if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration'); throw new Error("Failed to validate integration");
const environments = req.membership.workspace?.environments || [];
if(environments.length === 0){
throw new Error("Failed to get environments")
}
await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get OAuth2 code-token exchange'
});
}
return res.status(200).send({ const environments = req.membership.workspace?.environments || [];
message: 'Successfully enabled integration authorization' if (environments.length === 0) {
}); throw new Error("Failed to get environments");
}
await IntegrationService.handleOAuthExchange({
workspaceId,
integration,
code,
environment: environments[0].slug,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get OAuth2 code-token exchange",
});
}
return res.status(200).send({
message: "Successfully enabled integration authorization",
});
}; };
/** /**
* Save integration access token as part of integration [integration] for workspace with id [workspaceId] * Save integration access token as part of integration [integration] for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const saveIntegrationAccessToken = async ( export const saveIntegrationAccessToken = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
// TODO: refactor // TODO: refactor
let integrationAuth; let integrationAuth;
try { try {
const { const {
workspaceId, workspaceId,
accessToken, accessToken,
integration integration,
}: { }: {
workspaceId: string; workspaceId: string;
accessToken: string; accessToken: string;
integration: string; integration: string;
} = req.body; } = req.body;
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate(
workspace: new Types.ObjectId(workspaceId), {
integration workspace: new Types.ObjectId(workspaceId),
}, { integration,
workspace: new Types.ObjectId(workspaceId), },
integration {
}, { workspace: new Types.ObjectId(workspaceId),
new: true, integration,
upsert: true },
}); {
new: true,
upsert: true,
}
);
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
isActive: true isActive: true,
}); });
if (!bot) throw new Error('Bot must be enabled to save integration access token'); if (!bot)
throw new Error("Bot must be enabled to save integration access token");
// encrypt and save integration access token
integrationAuth = await IntegrationService.setIntegrationAuthAccess({ // encrypt and save integration access token
integrationAuthId: integrationAuth._id.toString(), integrationAuth = await IntegrationService.setIntegrationAuthAccess({
accessToken, integrationAuthId: integrationAuth._id.toString(),
accessExpiresAt: undefined accessToken,
}); accessExpiresAt: undefined,
});
if (!integrationAuth) throw new Error('Failed to save integration access token');
} catch (err) { if (!integrationAuth)
Sentry.setUser({ email: req.user.email }); throw new Error("Failed to save integration access token");
Sentry.captureException(err); } catch (err) {
return res.status(400).send({ Sentry.setUser({ email: req.user.email });
message: 'Failed to save access token for integration' Sentry.captureException(err);
}); return res.status(400).send({
} message: "Failed to save access token for integration",
});
return res.status(200).send({ }
integrationAuth
}); return res.status(200).send({
} integrationAuth,
});
};
/** /**
* Return list of applications allowed for integration with integration authorization id [integrationAuthId] * Return list of applications allowed for integration with integration authorization id [integrationAuthId]
@@ -127,23 +123,24 @@ export const saveIntegrationAccessToken = async (
* @returns * @returns
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps; let apps;
try { try {
apps = await getApps({ apps = await getApps({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken accessToken: req.accessToken,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); console.log(err); // testing
Sentry.captureException(err); Sentry.setUser({ email: req.user.email });
return res.status(400).send({ Sentry.captureException(err);
message: 'Failed to get integration authorization applications' return res.status(400).send({
}); message: "Failed to get integration authorization applications",
} });
}
return res.status(200).send({ return res.status(200).send({
apps apps,
}); });
}; };
/** /**
@@ -153,21 +150,21 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegrationAuth = async (req: Request, res: Response) => { export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth; let integrationAuth;
try { try {
integrationAuth = await revokeAccess({ integrationAuth = await revokeAccess({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken accessToken: req.accessToken,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete integration authorization' message: "Failed to delete integration authorization",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth,
}); });
} };
@@ -1,44 +1,40 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import * as Sentry from "@sentry/node";
import { import { Integration, Workspace, Bot, BotKey } from "../../models";
Integration, import { EventService } from "../../services";
Workspace, import { eventPushSecrets } from "../../events";
Bot,
BotKey
} from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
/** /**
* Create/initialize an (empty) integration for integration authorization * Create/initialize an (empty) integration for integration authorization
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; let integration;
try { try {
// initialize new integration after saving integration access token // initialize new integration after saving integration access token
integration = await new Integration({ integration = await new Integration({
workspace: req.integrationAuth.workspace._id, workspace: req.integrationAuth.workspace._id,
isActive: false, isActive: false,
app: null, app: null,
environment: req.integrationAuth.workspace?.environments[0].slug, environment: req.integrationAuth.workspace?.environments[0].slug,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: req.integrationAuth._id integrationAuth: req.integrationAuth._id,
}).save(); }).save();
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); console.log(err);
Sentry.captureException(err); Sentry.setUser({ email: req.user.email });
return res.status(400).send({ Sentry.captureException(err);
message: 'Failed to create integration' return res.status(400).send({
}); message: "Failed to create integration",
} });
}
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
} };
/** /**
* Change environment or name of integration with id [integrationId] * Change environment or name of integration with id [integrationId]
@@ -47,57 +43,57 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
let integration; let integration;
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
try {
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner
},
{
new: true
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update integration'
});
}
return res.status(200).send({ // TODO: add integration-specific validation to ensure that each
integration // integration has the correct fields populated in [Integration]
});
try {
const {
environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString(),
}),
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update integration",
});
}
return res.status(200).send({
integration,
});
}; };
/** /**
@@ -108,24 +104,24 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { export const deleteIntegration = async (req: Request, res: Response) => {
let integration; let integration;
try { try {
const { integrationId } = req.params; const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({ integration = await Integration.findOneAndDelete({
_id: integrationId _id: integrationId,
}); });
if (!integration) throw new Error('Failed to find integration'); if (!integration) throw new Error("Failed to find integration");
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
return res.status(400).send({ return res.status(400).send({
message: 'Failed to delete integration' message: "Failed to delete integration",
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
}; };
+5 -2
View File
@@ -321,9 +321,10 @@ const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => {
await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: { headers: {
"Circle-Token": accessToken, "Circle-Token": accessToken,
"Accept-Encoding": "application/json",
}, },
}) })
).data; ).data[0];
const { slug } = circleciOrganizationDetail; const { slug } = circleciOrganizationDetail;
@@ -333,15 +334,17 @@ const getAppsCircleci = async ({ accessToken }: { accessToken: string }) => {
{ {
headers: { headers: {
"Circle-Token": accessToken, "Circle-Token": accessToken,
"Accept-Encoding": "application/json",
}, },
} }
) )
).data.items; ).data?.items;
apps = res.map((a: any) => ({ apps = res.map((a: any) => ({
name: a?.project_slug?.split("/")[2], name: a?.project_slug?.split("/")[2],
})); }));
} catch (err) { } catch (err) {
console.log(err);
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error("Failed to get Render services"); throw new Error("Failed to get Render services");
+68 -15
View File
@@ -87,12 +87,12 @@ const syncSecrets = async ({
accessToken, accessToken,
}); });
break; break;
// case INTEGRATION_CIRCLECI: case INTEGRATION_CIRCLECI:
// await syncSecretsCircleci({ await syncSecretsCircleci({
// integration, integration,
// secrets, secrets,
// accessToken, accessToken,
// }); });
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -831,14 +831,67 @@ const syncSecretsFlyio = async ({
} }
}; };
// const syncSecretsCircleci = async ({ const syncSecretsCircleci = async ({
// integration, integration,
// secrets, secrets,
// accessToken, accessToken,
// }: { }: {
// integration: IIntegration; integration: IIntegration;
// secrets: any; secrets: any;
// accessToken: string; accessToken: string;
// }) => {}; }) => {
try {
const circleciOrganizationDetail = (
await axios.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
})
).data[0];
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;
console.log(getSecretsRes);
console.log(secrets);
// inject secrets to CircleCI
// note: no relivent api end point was found in CircleCI to do entire secrets at a same time so
// it is done one by one
Object.keys(secrets).forEach(
async (key) =>
await axios.post(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
{
name: key,
value: secrets[key],
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json",
},
}
)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to CircleCI");
}
};
export { syncSecrets }; export { syncSecrets };
+2
View File
@@ -6,6 +6,7 @@ import {
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
} from "../variables"; } from "../variables";
export interface IIntegration { export interface IIntegration {
@@ -74,6 +75,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_RENDER, INTEGRATION_RENDER,
INTEGRATION_FLYIO, INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
], ],
required: true, required: true,
}, },
+2
View File
@@ -4,6 +4,7 @@ import {
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_CIRCLECI,
} from "../variables"; } from "../variables";
export interface IIntegrationAuth { export interface IIntegrationAuth {
@@ -42,6 +43,7 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB, INTEGRATION_GITHUB,
INTEGRATION_CIRCLECI,
], ],
required: true, required: true,
}, },
+2 -2
View File
@@ -138,8 +138,8 @@ const INTEGRATION_OPTIONS = [
name: "Circle CI", name: "Circle CI",
slug: "circleci", slug: "circleci",
image: "Circle CI.png", image: "Circle CI.png",
isAvailable: false, isAvailable: true,
type: "", type: "pat",
clientId: "", clientId: "",
docsLink: "", docsLink: "",
}, },
+8 -16
View File
@@ -1,24 +1,16 @@
// membership roles // membership roles
const OWNER = 'owner'; const OWNER = "owner";
const ADMIN = 'admin'; const ADMIN = "admin";
const MEMBER = 'member'; const MEMBER = "member";
// membership statuses // membership statuses
const INVITED = 'invited'; const INVITED = "invited";
// membership permissions ability // membership permissions ability
const ABILITY_READ = 'read'; const ABILITY_READ = "read";
const ABILITY_WRITE = 'write'; const ABILITY_WRITE = "write";
// -- organization // -- organization
const ACCEPTED = 'accepted'; const ACCEPTED = "accepted";
export { export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE };
OWNER,
ADMIN,
MEMBER,
INVITED,
ACCEPTED,
ABILITY_READ,
ABILITY_WRITE
}
@@ -44,9 +44,9 @@ const CloudIntegration = ({
tabIndex={0} tabIndex={0}
className={`relative ${ className={`relative ${
cloudIntegrationOption.isAvailable cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer' ? 'cursor-pointer duration-200 hover:bg-white/10'
: 'opacity-50' : 'opacity-50'
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`} } flex h-32 flex-row items-center rounded-md bg-white/5 p-4`}
onClick={() => { onClick={() => {
if (!cloudIntegrationOption.isAvailable) return; if (!cloudIntegrationOption.isAvailable) return;
setSelectedIntegrationOption(cloudIntegrationOption); setSelectedIntegrationOption(cloudIntegrationOption);
@@ -61,22 +61,22 @@ const CloudIntegration = ({
alt="integration logo" alt="integration logo"
/> />
{cloudIntegrationOption.name.split(' ').length > 2 ? ( {cloudIntegrationOption.name.split(' ').length > 2 ? (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs"> <div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegrationOption.name.split(' ')[0]}</div> <div>{cloudIntegrationOption.name.split(' ')[0]}</div>
<div className="text-base"> <div className="text-base">
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]} {cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
</div> </div>
</div> </div>
) : ( ) : (
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs"> <div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegrationOption.name} {cloudIntegrationOption.name}
</div> </div>
)} )}
{cloudIntegrationOption.isAvailable && {cloudIntegrationOption.isAvailable &&
integrationAuths integrationAuths
.map((authorization) => authorization.integration) .map((authorization) => authorization?.integration)
.includes(cloudIntegrationOption.slug) && ( .includes(cloudIntegrationOption.slug) && (
<div className="absolute group z-40 top-0 right-0 flex flex-row"> <div className="group absolute top-0 right-0 z-40 flex flex-row">
<div <div
onKeyDown={() => null} onKeyDown={() => null}
role="button" role="button"
@@ -86,8 +86,7 @@ const CloudIntegration = ({
const deletedIntegrationAuth = await deleteIntegrationAuth({ const deletedIntegrationAuth = await deleteIntegrationAuth({
integrationAuthId: integrationAuths integrationAuthId: integrationAuths
.filter( .filter(
(authorization) => (authorization) => authorization.integration === cloudIntegrationOption.slug
authorization.integration === cloudIntegrationOption.slug
) )
.map((authorization) => authorization._id)[0] .map((authorization) => authorization._id)[0]
}); });
@@ -96,20 +95,20 @@ const CloudIntegration = ({
integrationAuth: deletedIntegrationAuth integrationAuth: deletedIntegrationAuth
}); });
}} }}
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200" className="flex w-max cursor-pointer flex-row items-center rounded-b-md bg-red py-0.5 px-2 text-xs opacity-0 duration-200 group-hover:opacity-100"
> >
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" /> <FontAwesomeIcon icon={faX} className="mr-2 py-px text-xs" />
Revoke Revoke
</div> </div>
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200"> <div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-90 duration-200 group-hover:opacity-100">
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" /> <FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized Authorized
</div> </div>
</div> </div>
)} )}
{!cloudIntegrationOption.isAvailable && ( {!cloudIntegrationOption.isAvailable && (
<div className="absolute group z-50 top-0 right-0 flex flex-row"> <div className="group absolute top-0 right-0 z-50 flex flex-row">
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90"> <div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-yellow py-0.5 px-2 text-xs text-black opacity-90">
Coming Soon Coming Soon
</div> </div>
</div> </div>
@@ -43,8 +43,8 @@ type Props = {
handleDeleteIntegration: (args: { integration: Integration }) => void; handleDeleteIntegration: (args: { integration: Integration }) => void;
}; };
const IntegrationTile = ({ const IntegrationTile = ({
integration, integration,
integrations, integrations,
bot, bot,
setBot, setBot,
@@ -54,7 +54,7 @@ const IntegrationTile = ({
}: Props) => { }: Props) => {
// set initial environment. This find will only execute when component is mounting // set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>( const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration.environment) || { environments.find(({ slug }) => slug === integration?.environment) || {
name: '', name: '',
slug: '' slug: ''
} }
@@ -66,27 +66,27 @@ const IntegrationTile = ({
useEffect(() => { useEffect(() => {
const loadIntegration = async () => { const loadIntegration = async () => {
const tempApps: [IntegrationApp] = await getIntegrationApps({ const tempApps: [IntegrationApp] = await getIntegrationApps({
integrationAuthId: integration.integrationAuth integrationAuthId: integration?.integrationAuth
}); });
setApps(tempApps); setApps(tempApps);
setIntegrationApp(integration.app ? integration.app : tempApps[0].name); setIntegrationApp(integration?.app ? integration.app : tempApps[0].name);
switch (integration.integration) { switch (integration.integration) {
case 'vercel': case 'vercel':
setIntegrationTargetEnvironment( setIntegrationTargetEnvironment(
integration?.targetEnvironment integration?.targetEnvironment
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1) ? integration.targetEnvironment.charAt(0).toUpperCase() +
: 'Development' integration.targetEnvironment.substring(1)
: 'Development'
); );
break; break;
case 'netlify': case 'netlify':
setIntegrationTargetEnvironment( setIntegrationTargetEnvironment(
integration?.targetEnvironment integration?.targetEnvironment
? contextNetlifyMapping[integration.targetEnvironment] ? contextNetlifyMapping[integration.targetEnvironment]
: 'Local development' : 'Local development'
); );
break; break;
default: default:
@@ -96,7 +96,7 @@ const IntegrationTile = ({
loadIntegration(); loadIntegration();
}, []); }, []);
const handleStartIntegration = async () => { const handleStartIntegration = async () => {
const reformatTargetEnvironment = (targetEnvironment: string) => { const reformatTargetEnvironment = (targetEnvironment: string) => {
switch (integration.integration) { switch (integration.integration) {
@@ -107,13 +107,13 @@ const IntegrationTile = ({
default: default:
return null; return null;
} }
} };
try { try {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const appId = siteApp?.appId ?? null; const appId = siteApp?.appId ?? null;
const owner = siteApp?.owner ?? null; const owner = siteApp?.owner ?? null;
// return updated integration // return updated integration
const updatedIntegration = await updateIntegration({ const updatedIntegration = await updateIntegration({
integrationId: integration._id, integrationId: integration._id,
@@ -124,15 +124,15 @@ const IntegrationTile = ({
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment), targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
owner owner
}); });
setIntegrations( setIntegrations(
integrations.map((i) => i._id === updatedIntegration._id ? updatedIntegration : i) integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
); );
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} };
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
const renderIntegrationSpecificParams = (integration: Integration) => { const renderIntegrationSpecificParams = (integration: Integration) => {
try { try {
@@ -140,7 +140,7 @@ const IntegrationTile = ({
case 'vercel': case 'vercel':
return ( return (
<div> <div>
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div> <div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
<ListBox <ListBox
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null} data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
isSelected={integrationTargetEnvironment} isSelected={integrationTargetEnvironment}
@@ -152,7 +152,7 @@ const IntegrationTile = ({
case 'netlify': case 'netlify':
return ( return (
<div> <div>
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div> <div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
<ListBox <ListBox
data={ data={
!integration.isActive !integration.isActive
@@ -177,10 +177,10 @@ const IntegrationTile = ({
if (!integrationApp || apps.length === 0) return <div />; if (!integrationApp || apps.length === 0) return <div />;
return ( return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between"> <div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md bg-white/5 p-6">
<div className="flex"> <div className="flex">
<div> <div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p> <p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
<ListBox <ListBox
data={!integration.isActive ? environments.map(({ name }) => name) : null} data={!integration.isActive ? environments.map(({ name }) => name) : null}
isSelected={integrationEnvironment.name} isSelected={integrationEnvironment.name}
@@ -196,16 +196,16 @@ const IntegrationTile = ({
/> />
</div> </div>
<div className="pt-2"> <div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" /> <FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
</div> </div>
<div className="mr-2"> <div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p> <p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</p>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300"> <div className="rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
</div> </div>
</div> </div>
<div className="mr-2"> <div className="mr-2">
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div> <div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
<ListBox <ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null} data={!integration.isActive ? apps.map((app) => app.name) : null}
isSelected={integrationApp} isSelected={integrationApp}
@@ -218,9 +218,9 @@ const IntegrationTile = ({
</div> </div>
<div className="flex items-end"> <div className="flex items-end">
{integration.isActive ? ( {integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4"> <div className="flex max-w-5xl flex-row items-center rounded-md bg-white/5 p-2 px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" /> <FontAwesomeIcon icon={faRotate} className="mr-2.5 animate-spin text-lg text-primary" />
<div className="text-gray-300 font-semibold">In Sync</div> <div className="font-semibold text-gray-300">In Sync</div>
</div> </div>
) : ( ) : (
<Button <Button
@@ -230,11 +230,13 @@ const IntegrationTile = ({
size="md" size="md"
/> />
)} )}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2"> <div className="ml-2 opacity-50 duration-200 hover:opacity-100">
<Button <Button
onButtonPressed={() => handleDeleteIntegration({ onButtonPressed={() =>
integration handleDeleteIntegration({
})} integration
})
}
color="red" color="red"
size="icon-md" size="icon-md"
icon={faX} icon={faX}
@@ -24,7 +24,7 @@ interface Integration {
} }
const ProjectIntegrationSection = ({ const ProjectIntegrationSection = ({
integrations, integrations,
setIntegrations, setIntegrations,
bot, bot,
setBot, setBot,
@@ -33,22 +33,20 @@ const ProjectIntegrationSection = ({
}: Props) => { }: Props) => {
return integrations.length > 0 ? ( return integrations.length > 0 ? (
<div className="mb-12"> <div className="mb-12">
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2"> <div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="font-semibold text-3xl">Current Integrations</h1> <h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-gray-400"> <p className="text-base text-gray-400">Manage integrations with third-party services.</p>
Manage integrations with third-party services.
</p>
</div> </div>
{integrations.map((integration: Integration) => { {integrations.map((integration: Integration) => {
return ( return (
<IntegrationTile <IntegrationTile
key={`integration-${integration._id.toString()}`} key={`integration-${integration?._id.toString()}`}
integration={integration} integration={integration}
integrations={integrations} integrations={integrations}
bot={bot} bot={bot}
setBot={setBot} setBot={setBot}
setIntegrations={setIntegrations} setIntegrations={setIntegrations}
environments={environments} environments={environments}
handleDeleteIntegration={handleDeleteIntegration} handleDeleteIntegration={handleDeleteIntegration}
/> />
); );
@@ -57,6 +55,6 @@ const ProjectIntegrationSection = ({
) : ( ) : (
<div /> <div />
); );
} };
export default ProjectIntegrationSection; export default ProjectIntegrationSection;