Add Render, Fly.io integrations and reduce integrations page reloads

This commit is contained in:
Tuan Dang
2023-01-22 00:09:37 +07:00
parent 06803519e6
commit 0c351c0925
45 changed files with 1096 additions and 350 deletions
@@ -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;