mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 20:27:43 +00:00
Add Render, Fly.io integrations and reduce integrations page reloads
This commit is contained in:
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -4,12 +4,23 @@ import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
selectedIntegrationOption: never[] | null;
|
||||
selectedIntegrationOption: IntegrationOption | null;
|
||||
handleBotActivate: () => Promise<void>;
|
||||
handleIntegrationOption: (arg: { integrationOption: never[] }) => void;
|
||||
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||
};
|
||||
|
||||
const ActivateBotDialog = ({
|
||||
@@ -17,7 +28,7 @@ const ActivateBotDialog = ({
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
integrationOptionPress
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -28,10 +39,10 @@ const ActivateBotDialog = ({
|
||||
|
||||
// type check
|
||||
if (!selectedIntegrationOption) return;
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
|
||||
// 2. start integration or probe for PAT
|
||||
integrationOptionPress(selectedIntegrationOption);
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
@@ -1,45 +1,60 @@
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
selectedIntegrationOption: string;
|
||||
handleBotActivate: () => void;
|
||||
handleIntegrationOption: (arg:{integrationOption:string})=>void;
|
||||
selectedIntegrationOption: IntegrationOption | null
|
||||
handleIntegrationOption: (arg:{
|
||||
integrationOption: IntegrationOption,
|
||||
accessToken?: string;
|
||||
})=>void;
|
||||
};
|
||||
|
||||
const IntegrationAccessTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
}:Props) => {
|
||||
|
||||
const [accessToken, setAccessToken] = useState('');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. activate bot
|
||||
await handleBotActivate();
|
||||
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
if (selectedIntegrationOption && accessToken !== '') {
|
||||
handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption,
|
||||
accessToken
|
||||
});
|
||||
closeModal();
|
||||
setAccessToken('');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Dialog as="div" className="relative z-10" onClose={() => {
|
||||
console.log('onClose');
|
||||
closeModal();
|
||||
}}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@@ -67,28 +82,30 @@ const IntegrationAccessTokenDialog = ({
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
Grant Infisical access to your secrets
|
||||
{`Enter your ${selectedIntegrationOption?.name} API Key`}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over.
|
||||
{`This integration requires you to obtain an API key from ${selectedIntegrationOption?.name ?? ''} and store it with Infisical.`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
{/* <Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Grant access"
|
||||
size="md"
|
||||
/> */}
|
||||
<InputField
|
||||
label="Access token"
|
||||
onChangeHandler={() => {}}
|
||||
label="API Key"
|
||||
onChangeHandler={setAccessToken}
|
||||
type="varName"
|
||||
value="Hello"
|
||||
value={accessToken}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Connect"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import deleteIntegrationAuth from '../../pages/api/integrations/DeleteIntegrationAuth';
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOption: CloudIntegrationOption;
|
||||
setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
|
||||
integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
|
||||
cloudIntegrationOption: IntegrationOption;
|
||||
setSelectedIntegrationOption: (cloudIntegration: IntegrationOption) => void;
|
||||
integrationOptionPress: (cloudIntegrationOption: IntegrationOption) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||
}
|
||||
|
||||
const CloudIntegration = ({
|
||||
cloudIntegrationOption,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths
|
||||
integrationAuths,
|
||||
handleDeleteIntegrationAuth
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
return integrationAuths ? (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
@@ -51,7 +55,7 @@ const CloudIntegration = ({
|
||||
key={cloudIntegrationOption.name}
|
||||
>
|
||||
<Image
|
||||
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
||||
src={`/images/integrations/${cloudIntegrationOption.image}`}
|
||||
height={70}
|
||||
width={70}
|
||||
alt="integration logo"
|
||||
@@ -71,24 +75,26 @@ const CloudIntegration = ({
|
||||
{cloudIntegrationOption.isAvailable &&
|
||||
integrationAuths
|
||||
.map((authorization) => authorization.integration)
|
||||
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
||||
.includes(cloudIntegrationOption.slug) && (
|
||||
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(event) => {
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
deleteIntegrationAuth({
|
||||
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
||||
integrationAuthId: integrationAuths
|
||||
.filter(
|
||||
(authorization) =>
|
||||
authorization.integration === cloudIntegrationOption.name.toLowerCase()
|
||||
authorization.integration === cloudIntegrationOption.slug
|
||||
)
|
||||
.map((authorization) => authorization._id)[0]
|
||||
});
|
||||
|
||||
router.reload();
|
||||
handleDeleteIntegrationAuth({
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -2,20 +2,31 @@ import { useTranslation } from 'next-i18next';
|
||||
|
||||
import CloudIntegration from './CloudIntegration';
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOptions: CloudIntegrationOption[];
|
||||
cloudIntegrationOptions: IntegrationOption[];
|
||||
setSelectedIntegrationOption: () => void;
|
||||
integrationOptionPress: () => void;
|
||||
integrationAuths: any;
|
||||
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||
}
|
||||
|
||||
const CloudIntegrationSection = ({
|
||||
@@ -23,6 +34,7 @@ const CloudIntegrationSection = ({
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths,
|
||||
handleDeleteIntegrationAuth
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -45,6 +57,7 @@ const CloudIntegrationSection = ({
|
||||
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
integrationAuths={integrationAuths}
|
||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -13,39 +13,44 @@ import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
|
||||
import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
|
||||
import updateIntegration from '../../pages/api/integrations/updateIntegration';
|
||||
|
||||
interface TIntegration {
|
||||
interface Integration {
|
||||
_id: string;
|
||||
app?: string;
|
||||
target?: string;
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
isActive: boolean;
|
||||
context: string;
|
||||
}
|
||||
|
||||
interface IntegrationApp {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
integration: TIntegration;
|
||||
integrations: TIntegration[];
|
||||
integration: Integration;
|
||||
integrations: Integration[];
|
||||
setIntegrations: any;
|
||||
bot: any;
|
||||
setBot: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||
};
|
||||
|
||||
const Integration = ({
|
||||
const IntegrationTile = ({
|
||||
integration,
|
||||
integrations,
|
||||
bot,
|
||||
setBot,
|
||||
setIntegrations,
|
||||
environments = []
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
// set initial environment. This find will only execute when component is mounting
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||
@@ -57,8 +62,7 @@ const Integration = ({
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||
const [integrationApp, setIntegrationApp] = useState(''); // integration app name
|
||||
const [integrationTarget, setIntegrationTarget] = useState(''); // vercel-specific integration param
|
||||
const [integrationContext, setIntegrationContext] = useState(''); // netlify-specific integration param
|
||||
const [integrationTargetEnvironment, setIntegrationTargetEnvironment] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const loadIntegration = async () => {
|
||||
@@ -66,21 +70,23 @@ const Integration = ({
|
||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||
integrationAuthId: integration.integrationAuth
|
||||
});
|
||||
|
||||
|
||||
setApps(tempApps);
|
||||
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
||||
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
setIntegrationTarget(
|
||||
integration?.target
|
||||
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
||||
: 'Development'
|
||||
setIntegrationTargetEnvironment(
|
||||
integration?.targetEnvironment
|
||||
? integration.targetEnvironment.charAt(0).toUpperCase() + integration.targetEnvironment.substring(1)
|
||||
: 'Development'
|
||||
);
|
||||
break;
|
||||
case 'netlify':
|
||||
setIntegrationContext(
|
||||
integration?.context ? contextNetlifyMapping[integration.context] : 'Local development'
|
||||
setIntegrationTargetEnvironment(
|
||||
integration?.targetEnvironment
|
||||
? contextNetlifyMapping[integration.targetEnvironment]
|
||||
: 'Local development'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -92,22 +98,30 @@ const Integration = ({
|
||||
}, []);
|
||||
|
||||
const handleStartIntegration = async () => {
|
||||
const reformatTargetEnvironment = (targetEnvironment: string) => {
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
return targetEnvironment.toLowerCase();
|
||||
case 'netlify':
|
||||
return reverseContextNetlifyMapping[targetEnvironment];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||
const siteId = siteApp?.siteId ?? null;
|
||||
const appId = siteApp?.appId ?? null;
|
||||
const owner = siteApp?.owner ?? null;
|
||||
|
||||
// return updated integration
|
||||
const updatedIntegration = await updateIntegration({
|
||||
integrationId: integration._id,
|
||||
environment: integrationEnvironment.slug,
|
||||
app: integrationApp,
|
||||
isActive: true,
|
||||
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
||||
context: integrationContext
|
||||
? reverseContextNetlifyMapping[integrationContext]
|
||||
: null,
|
||||
siteId,
|
||||
app: integrationApp,
|
||||
appId,
|
||||
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
|
||||
owner
|
||||
});
|
||||
|
||||
@@ -119,30 +133,8 @@ const Integration = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteIntegration = async () => {
|
||||
try {
|
||||
const deletedIntegration = await deleteIntegration({
|
||||
integrationId: integration._id
|
||||
});
|
||||
|
||||
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
|
||||
setIntegrations(newIntegrations);
|
||||
|
||||
if (newIntegrations.length < 1) {
|
||||
// case: no integrations left
|
||||
setBot({
|
||||
...bot,
|
||||
isActive: false
|
||||
})
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const renderIntegrationSpecificParams = (integration: TIntegration) => {
|
||||
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
@@ -151,8 +143,8 @@ const Integration = ({
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
||||
isSelected={integrationTarget}
|
||||
onChange={setIntegrationTarget}
|
||||
isSelected={integrationTargetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
isFull
|
||||
/>
|
||||
</div>
|
||||
@@ -167,8 +159,8 @@ const Integration = ({
|
||||
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
|
||||
: null
|
||||
}
|
||||
isSelected={integrationContext}
|
||||
onChange={setIntegrationContext}
|
||||
isSelected={integrationTargetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -240,7 +232,9 @@ const Integration = ({
|
||||
)}
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||
<Button
|
||||
onButtonPressed={() => handleDeleteIntegration()}
|
||||
onButtonPressed={() => handleDeleteIntegration({
|
||||
integration
|
||||
})}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faX}
|
||||
@@ -251,4 +245,4 @@ const Integration = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default Integration;
|
||||
export default IntegrationTile;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
|
||||
import Integration from './Integration';
|
||||
import IntegrationTile from './Integration';
|
||||
|
||||
interface Props {
|
||||
integrations: any;
|
||||
@@ -8,16 +6,21 @@ interface Props {
|
||||
bot: any;
|
||||
setBot: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||
}
|
||||
|
||||
interface IntegrationType {
|
||||
interface Integration {
|
||||
_id: string;
|
||||
app?: string;
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
isActive: boolean;
|
||||
context: string;
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({
|
||||
@@ -25,7 +28,8 @@ const ProjectIntegrationSection = ({
|
||||
setIntegrations,
|
||||
bot,
|
||||
setBot,
|
||||
environments = []
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) =>
|
||||
integrations.length > 0 ? (
|
||||
<div className="mb-12">
|
||||
@@ -35,20 +39,24 @@ const ProjectIntegrationSection = ({
|
||||
Manage integrations with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
{integrations.map((integration: IntegrationType) => (
|
||||
<Integration
|
||||
key={guidGenerator()}
|
||||
integration={integration}
|
||||
integrations={integrations}
|
||||
bot={bot}
|
||||
setBot={setBot}
|
||||
setIntegrations={setIntegrations}
|
||||
environments={environments}
|
||||
/>
|
||||
))}
|
||||
{integrations.map((integration: Integration) => {
|
||||
console.log('IntegrationSection integration: ', integration);
|
||||
return (
|
||||
<IntegrationTile
|
||||
key={`integration-${integration._id.toString()}`}
|
||||
integration={integration}
|
||||
integrations={integrations}
|
||||
bot={bot}
|
||||
setBot={setBot}
|
||||
setIntegrations={setIntegrations}
|
||||
environments={environments}
|
||||
handleDeleteIntegration={handleDeleteIntegration}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
|
||||
export default ProjectIntegrationSection;
|
||||
export default ProjectIntegrationSection;
|
||||
@@ -8,7 +8,7 @@ interface BotKey {
|
||||
interface Props {
|
||||
botId: string;
|
||||
isActive: boolean;
|
||||
botKey: BotKey;
|
||||
botKey?: BotKey;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ const deleteIntegrationAuth = ({ integrationAuthId }: Props) =>
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res;
|
||||
return (await res.json()).integrationAuth;
|
||||
}
|
||||
console.log('Failed to delete an integration authorization');
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
integrationAuthId: string;
|
||||
}
|
||||
/**
|
||||
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - id of integration authorization for which to create the integration
|
||||
* @returns
|
||||
*/
|
||||
const createIntegration = ({
|
||||
integrationAuthId
|
||||
}: Props) =>
|
||||
SecurityClient.fetchCall('/api/v1/integration', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
integrationAuthId
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).integration;
|
||||
}
|
||||
console.log('Failed to create integration');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default createIntegration;
|
||||
@@ -0,0 +1,42 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
workspaceId: string | null;
|
||||
integration: string | undefined;
|
||||
accessToken: string;
|
||||
}
|
||||
/**
|
||||
* This route creates a new integration authorization for integration [integration]
|
||||
* that requires the user to input their access token manually (e.g. Render). It
|
||||
* saves access token [accessToken] under that integration for workspace with id
|
||||
* [workspaceId].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace to authorize integration for
|
||||
* @param {String} obj.integration - integration
|
||||
* @param {String} obj.accessToken - access token to save
|
||||
* @returns
|
||||
*/
|
||||
const saveIntegrationAccessToken = ({
|
||||
workspaceId,
|
||||
integration,
|
||||
accessToken
|
||||
}: Props) =>
|
||||
SecurityClient.fetchCall(`/api/v1/integration-auth/access-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
workspaceId,
|
||||
integration,
|
||||
accessToken
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).integrationAuth;
|
||||
}
|
||||
console.log('Failed to save integration access token');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default saveIntegrationAccessToken;
|
||||
@@ -6,32 +6,29 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
* [environment] to the integration [app] with active state [isActive]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationId - id of integration
|
||||
* @param {String} obj.app - name of app
|
||||
* @param {String} obj.environment - project environment to push secrets from
|
||||
* @param {Boolean} obj.isActive - active state
|
||||
* @param {String} obj.target - (optional) target (environment) for Vercel integration
|
||||
* @param {String} obj.context - (optional) context (environment) for Netlify integration
|
||||
* @param {String} obj.siteId - (optional) app (site_id) for Netlify integration
|
||||
* @param {String} obj.environment - project environment to push secrets from
|
||||
* @param {String} obj.app - name of app
|
||||
* @param {String} obj.appId - (optional) app ID for integration
|
||||
* @param {String} obj.targetEnvironment - target environment for integration
|
||||
* @param {String} obj.owner - (optional) owner login of repo for GitHub integration
|
||||
* @returns
|
||||
*/
|
||||
const updateIntegration = ({
|
||||
integrationId,
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target,
|
||||
context,
|
||||
siteId,
|
||||
environment,
|
||||
app,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner
|
||||
}: {
|
||||
integrationId: string;
|
||||
app: string;
|
||||
environment: string;
|
||||
isActive: boolean;
|
||||
target: string | null;
|
||||
context: string | null;
|
||||
siteId: string | null;
|
||||
environment: string;
|
||||
app: string;
|
||||
appId: string | null;
|
||||
targetEnvironment: string | null;
|
||||
owner: string | null;
|
||||
}) =>
|
||||
SecurityClient.fetchCall(`/api/v1/integration/${integrationId}`, {
|
||||
@@ -43,9 +40,8 @@ const updateIntegration = ({
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target,
|
||||
context,
|
||||
siteId,
|
||||
appId,
|
||||
targetEnvironment,
|
||||
owner
|
||||
})
|
||||
}).then(async (res) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import frameworkIntegrationOptions from 'public/json/frameworkIntegrations.json';
|
||||
|
||||
import ActivateBotDialog from '@app/components/basic/dialog/ActivateBotDialog';
|
||||
import IntegrationAccessTokenDialog from '@app/components/basic/dialog/IntegrationAccessTokenDialog';
|
||||
import CloudIntegrationSection from '@app/components/integrations/CloudIntegrationSection';
|
||||
import FrameworkIntegrationSection from '@app/components/integrations/FrameworkIntegrationSection';
|
||||
import IntegrationSection from '@app/components/integrations/IntegrationSection';
|
||||
@@ -19,27 +20,63 @@ import {
|
||||
} from '../../components/utilities/cryptography/crypto';
|
||||
import getBot from '../api/bot/getBot';
|
||||
import setBotActiveStatus from '../api/bot/setBotActiveStatus';
|
||||
import createIntegration from '../api/integrations/createIntegration';
|
||||
import deleteIntegration from '../api/integrations/DeleteIntegration';
|
||||
import getIntegrationOptions from '../api/integrations/GetIntegrationOptions';
|
||||
import getWorkspaceAuthorizations from '../api/integrations/getWorkspaceAuthorizations';
|
||||
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
|
||||
import saveIntegrationAccessToken from '../api/integrations/saveIntegrationAccessToken';
|
||||
import getAWorkspace from '../api/workspace/getAWorkspace';
|
||||
import getLatestFileKey from '../api/workspace/getLatestFileKey';
|
||||
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Integration {
|
||||
_id: string;
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
}
|
||||
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default function Integrations() {
|
||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
||||
const [integrationAuths, setIntegrationAuths] = useState([]);
|
||||
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
|
||||
const [environments, setEnvironments] = useState<
|
||||
{
|
||||
name: string;
|
||||
slug: string;
|
||||
}[]
|
||||
>([]);
|
||||
const [integrations, setIntegrations] = useState([]);
|
||||
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
||||
// TODO: These will have its type when migratiing towards react-query
|
||||
const [bot, setBot] = useState<any>(null);
|
||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
||||
// const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(true);
|
||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
||||
const [isIntegrationAccessTokenDialogOpen, setIntegrationAccessTokenDialogOpen] = useState(false);
|
||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
const workspaceId = router.query.id as string;
|
||||
@@ -72,7 +109,7 @@ export default function Integrations() {
|
||||
// get project bot
|
||||
setBot(await getBot({ workspaceId }));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
@@ -118,7 +155,7 @@ export default function Integrations() {
|
||||
(
|
||||
await setBotActiveStatus({
|
||||
botId: bot._id,
|
||||
isActive: !bot.isActive,
|
||||
isActive: true,
|
||||
botKey
|
||||
})
|
||||
).bot
|
||||
@@ -128,9 +165,9 @@ export default function Integrations() {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Start integration for a given integration option [integrationOption]
|
||||
* Handle integration option authorization for a given integration option [integrationOption]
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.integrationOption - an integration option
|
||||
* @param {String} obj.name
|
||||
@@ -138,45 +175,68 @@ export default function Integrations() {
|
||||
* @param {String} obj.docsLink
|
||||
* @returns
|
||||
*/
|
||||
const handleIntegrationOption = async ({ integrationOption }: { integrationOption: any }) => {
|
||||
const handleIntegrationOption = async ({
|
||||
integrationOption,
|
||||
accessToken
|
||||
}: {
|
||||
integrationOption: IntegrationOption,
|
||||
accessToken?: string;
|
||||
}) => {
|
||||
try {
|
||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||
const state = crypto.randomBytes(16).toString('hex');
|
||||
localStorage.setItem('latestCSRFToken', state);
|
||||
if (integrationOption.type === 'oauth') {
|
||||
// integration is of type OAuth
|
||||
|
||||
switch (integrationOption.name) {
|
||||
case 'Heroku':
|
||||
window.location.assign(
|
||||
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'Vercel':
|
||||
window.location.assign(
|
||||
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'Netlify':
|
||||
window.location.assign(
|
||||
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
|
||||
);
|
||||
break;
|
||||
case 'GitHub':
|
||||
window.location.assign(
|
||||
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
// case 'Fly.io':
|
||||
// console.log('fly.io');
|
||||
// setIntegrationAccessTokenDialogOpen(true);
|
||||
// break;
|
||||
// generate CSRF token for OAuth2 code-token exchange integrations
|
||||
const state = crypto.randomBytes(16).toString('hex');
|
||||
localStorage.setItem('latestCSRFToken', state);
|
||||
|
||||
switch (integrationOption.slug) {
|
||||
case 'heroku':
|
||||
window.location.assign(
|
||||
`https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'vercel':
|
||||
window.location.assign(
|
||||
`https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`
|
||||
);
|
||||
break;
|
||||
case 'netlify':
|
||||
window.location.assign(
|
||||
`https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/netlify`
|
||||
);
|
||||
break;
|
||||
case 'github':
|
||||
window.location.assign(
|
||||
`https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/github&state=${state}`
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
} if (integrationOption.type === 'pat') {
|
||||
// integration is of type personal access token
|
||||
const integrationAuth = await saveIntegrationAccessToken({
|
||||
workspaceId: localStorage.getItem('projectData.id'),
|
||||
integration: integrationOption.slug,
|
||||
accessToken: accessToken ?? ''
|
||||
});
|
||||
|
||||
setIntegrationAuths([...integrationAuths, integrationAuth])
|
||||
|
||||
const integration = await createIntegration({
|
||||
integrationAuthId: integrationAuth._id
|
||||
});
|
||||
|
||||
setIntegrations([...integrations, integration]);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Open dialog to activate bot if bot is not active.
|
||||
* Otherwise, start integration [integrationOption]
|
||||
@@ -186,20 +246,94 @@ export default function Integrations() {
|
||||
* @param {String} integrationOption.docsLink
|
||||
* @returns
|
||||
*/
|
||||
const integrationOptionPress = (integrationOption: any) => {
|
||||
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
|
||||
try {
|
||||
if (bot.isActive) {
|
||||
// case: bot is active -> proceed with integration
|
||||
const integrationAuthX = integrationAuths.find((integrationAuth) => integrationAuth.integration === integrationOption.slug);
|
||||
|
||||
if (!integrationAuthX) {
|
||||
// case: integration has not been authorized before
|
||||
|
||||
if (integrationOption.type === 'pat') {
|
||||
// case: integration requires user to input their personal access token for that integration
|
||||
setIntegrationAccessTokenDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// case: integration does not require user to input their personal access token (i.e. it's an OAuth2 integration)
|
||||
handleIntegrationOption({ integrationOption });
|
||||
return;
|
||||
}
|
||||
|
||||
// case: bot is not active -> open modal to activate bot
|
||||
setIsActivateBotDialogOpen(true);
|
||||
|
||||
// case: integration has been authorized before
|
||||
// -> create new integration
|
||||
const integration = await createIntegration({
|
||||
integrationAuthId: integrationAuthX._id
|
||||
});
|
||||
|
||||
setIntegrations([...integrations, integration]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
|
||||
* @param {Object} obj
|
||||
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
|
||||
*/
|
||||
const handleDeleteIntegrationAuth = async ({ integrationAuth: deletedIntegrationAuth }: { integrationAuth: IntegrationAuth }) => {
|
||||
try {
|
||||
const newIntegrations = integrations.filter((integration) => integration.integrationAuth !== deletedIntegrationAuth._id);
|
||||
setIntegrationAuths(integrationAuths.filter((integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id));
|
||||
setIntegrations(newIntegrations);
|
||||
|
||||
// handle updating bot
|
||||
if (newIntegrations.length < 1) {
|
||||
// case: no integrations left
|
||||
setBot(
|
||||
(
|
||||
await setBotActiveStatus({
|
||||
botId: bot._id,
|
||||
isActive: false
|
||||
})
|
||||
).bot
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deleting integration [integration]
|
||||
* @param {Object} obj
|
||||
* @param {Integration} obj.integration - integration to delete
|
||||
*/
|
||||
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
|
||||
try {
|
||||
const deletedIntegration = await deleteIntegration({
|
||||
integrationId: integration._id
|
||||
});
|
||||
|
||||
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
|
||||
setIntegrations(newIntegrations);
|
||||
|
||||
// handle updating bot
|
||||
if (newIntegrations.length < 1) {
|
||||
// case: no integrations left
|
||||
setBot(
|
||||
(
|
||||
await setBotActiveStatus({
|
||||
botId: bot._id,
|
||||
isActive: false
|
||||
})
|
||||
).bot
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
@@ -217,28 +351,37 @@ export default function Integrations() {
|
||||
closeModal={() => setIsActivateBotDialogOpen(false)}
|
||||
selectedIntegrationOption={selectedIntegrationOption}
|
||||
handleBotActivate={handleBotActivate}
|
||||
handleIntegrationOption={handleIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
/>
|
||||
{/* <IntegrationAccessTokenDialog
|
||||
<IntegrationAccessTokenDialog
|
||||
isOpen={isIntegrationAccessTokenDialogOpen}
|
||||
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
|
||||
selectedIntegrationOption={selectedIntegrationOption}
|
||||
handleBotActivate={handleBotActivate}
|
||||
handleIntegrationOption={handleIntegrationOption}
|
||||
/> */}
|
||||
|
||||
/>
|
||||
<IntegrationSection
|
||||
integrations={integrations}
|
||||
setIntegrations={setIntegrations}
|
||||
bot={bot}
|
||||
setBot={setBot}
|
||||
environments={environments}
|
||||
handleDeleteIntegration={handleDeleteIntegration}
|
||||
/>
|
||||
{cloudIntegrationOptions.length > 0 && bot ? (
|
||||
<CloudIntegrationSection
|
||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
||||
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
|
||||
integrationOptionPress={integrationOptionPress as any}
|
||||
integrationOptionPress={(integrationOption: IntegrationOption) => {
|
||||
if (!bot.isActive) {
|
||||
// case: bot is not active -> open modal to activate bot
|
||||
setIsActivateBotDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
integrationOptionPress(integrationOption)
|
||||
}}
|
||||
integrationAuths={integrationAuths}
|
||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
|
||||
Reference in New Issue
Block a user