Merge branch 'main' into auto-capitalization-toggle

This commit is contained in:
Hasan Mansoor
2023-02-08 05:23:41 +03:00
committed by GitHub
82 changed files with 2924 additions and 682 deletions
@@ -26,7 +26,7 @@ const AuthorizeIntegration = ({ workspaceId, code, integration }: Props) =>
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
return (await res.json()).integrationAuth;
}
console.log('Failed to authorize the integration');
return undefined;
@@ -1,7 +1,13 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
integrationAuthId: string;
integrationAuthId: string;
isActive: boolean;
app: string | null;
appId: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
owner: string | null;
}
/**
* This route creates a new integration based on the integration authorization with id [integrationAuthId]
@@ -10,7 +16,13 @@ interface Props {
* @returns
*/
const createIntegration = ({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
}: Props) =>
SecurityClient.fetchCall('/api/v1/integration', {
method: 'POST',
@@ -18,7 +30,13 @@ const createIntegration = ({
'Content-Type': 'application/json'
},
body: JSON.stringify({
integrationAuthId
integrationAuthId,
isActive,
app,
appId,
sourceEnvironment,
targetEnvironment,
owner
})
}).then(async (res) => {
if (res && res.status === 200) {
@@ -0,0 +1,22 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
/**
* This route lets us get the tags for a certain project
* @param {string} workspaceId
* @returns
*/
const getWorkspaceTags = ({ workspaceId }: { workspaceId: string }) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/tags`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).workspaceTags;
}
console.log('Failed to get the tags available in a certain project');
return undefined;
});
export default getWorkspaceTags;
+118 -53
View File
@@ -14,9 +14,11 @@ import {
faEyeSlash,
faFolderOpen,
faMagnifyingGlass,
faPlus
faPlus,
faXmark
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox';
@@ -44,6 +46,7 @@ import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
type WorkspaceEnv = {
name: string;
@@ -59,6 +62,7 @@ interface SecretDataProps {
id: string;
idOverride: string | undefined;
comment: string;
tags: Tag[];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -82,6 +86,7 @@ interface SnapshotProps {
value: string;
valueOverride: string;
comment: string;
tags: Tag[];
}[];
}
@@ -127,6 +132,7 @@ export default function Dashboard() {
const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false);
const [autoCapitalization, setAutoCapitalization] = useState(false);
const [projectTags, setProjectTags] = useState<Tag[]>([]);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -235,6 +241,8 @@ export default function Dashboard() {
action: 'first_time_secrets_pushed'
});
setHasUserEverPushed(!!userAction);
setProjectTags(await getWorkspaceTags({ workspaceId }));
} catch (error) {
console.log('Error', error);
setData(undefined);
@@ -277,7 +285,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
comment: ''
comment: '',
tags: []
},
...data!
]);
@@ -297,7 +306,8 @@ export default function Dashboard() {
key: '',
value: '',
valueOverride: undefined,
comment: ''
comment: '',
tags: []
},
]);
};
@@ -335,6 +345,11 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
setButtonReady(true);
};
// For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos);
@@ -352,6 +367,10 @@ export default function Dashboard() {
modifyComment(value, pos);
}, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
modifyTags(value, pos);
}, []);
/**
* Save the changes of environment variables and push them to the database
*/
@@ -424,7 +443,9 @@ export default function Dashboard() {
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment)
initDataPoint.comment) ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].tags !==
initDataPoint.tags
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -458,7 +479,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
@@ -473,7 +495,9 @@ export default function Dashboard() {
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment)
.comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.tags !== initDataPoint.tags)
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
@@ -485,7 +509,8 @@ export default function Dashboard() {
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride)
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
@@ -546,34 +571,6 @@ export default function Dashboard() {
<meta name="og:description" content={String(t('dashboard:og-description'))} />
</Head>
<div className="flex flex-row h-full dark:[color-scheme:dark]">
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
<div className="w-full max-h-96 pb-2 dark:[color-scheme:dark]">
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
{checkDocsPopUpVisible && (
@@ -628,7 +625,10 @@ export default function Dashboard() {
<div className="flex justify-start max-w-sm mt-1 mr-2">
<Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => togglePITSidebar(true)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
@@ -663,7 +663,8 @@ export default function Dashboard() {
valueOverride: sv.valueOverride,
key: sv.key,
value: sv.value,
comment: ''
comment: '',
tags: sv.tags
}));
setData(rolledBackSecrets);
@@ -781,27 +782,58 @@ export default function Dashboard() {
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
>
<div ref={secretsTop} />
<div className='bg-mineshaft-800 text-sm rounded-t-md h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
<div className='w-14'/>
<div className='text-bunker-300 relative font-semibold h-10 flex items-center w-80 pl-2 border-r border-mineshaft-600'>
<span>Key</span>
{!snapshotData && <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
<div
className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
>
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-2/12 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-transparent text-xs flex items-center justify-center w-14 h-10 cursor-default'>0</div>
<span className='px-2 text-bunker-300 font-semibold'>Key</span>
{!snapshotData && <IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => reorderRows(1)}
>
{sortMethod === 'alphabetical' ? <FontAwesomeIcon icon={faArrowUp} /> : <FontAwesomeIcon icon={faArrowDown} />}
</IconButton>}
</div>
<div className="w-5/12 border-r border-mineshaft-600">
<div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
>
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
</div>
</div>
<div className="w-2/12 border-r border-mineshaft-600">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
</div>
</div>
<div className="w-2/12">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
</div>
</div>
<div
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
/>
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
<div
onKeyDown={() => null}
role="none"
onClick={() => {}}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
</div>
</div>
</div>
<div className='text-bunker-300 pl-2 font-semibold h-10 flex items-center w-full border-r border-mineshaft-600'>Value</div>
<div className='text-bunker-300 pl-2 font-semibold h-10 flex items-center w-96'>Comment</div>
{!snapshotData && <div className='w-[9.3rem]'/>}
</div>
<div className="bg-mineshaft-800 rounded-b-md border-bunker-600">
{!snapshotData &&
data
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase()) || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id))
.map((keyPair) => (
<KeyPair
@@ -812,14 +844,17 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
)}
toggleSidebar={toggleSidebar}
togglePITSidebar={togglePITSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
deleteRow={deleteCertainRow}
tags={projectTags}
/>
))}
{snapshotData &&
@@ -852,6 +887,7 @@ export default function Dashboard() {
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
modifyComment={listenChangeComment}
modifyTags={listenChangeTags}
isBlurred={blurred}
isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes(
keyPair.key
@@ -859,6 +895,7 @@ export default function Dashboard() {
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot
tags={projectTags}
/>
))}
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
@@ -912,6 +949,34 @@ export default function Dashboard() {
)}
</div>
</div>
{sidebarSecretId !== 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key === data.filter((r) => r.id === sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv!}
workspaceId={workspaceId}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
</div>
</div>
) : (
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Github() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Github integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Github.requireAuth = true;
-41
View File
@@ -1,41 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Heroku() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (error) {
console.error('Heroku integration error: ', error);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Heroku.requireAuth = true;
+78 -98
View File
@@ -7,7 +7,6 @@ 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';
@@ -20,12 +19,10 @@ 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';
@@ -52,6 +49,7 @@ interface Integration {
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
@@ -75,7 +73,6 @@ export default function Integrations() {
// 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(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState<IntegrationOption | null>(null);
const router = useRouter();
@@ -166,80 +163,83 @@ export default function Integrations() {
}
};
/**
* Handle integration option authorization for a given integration option [integrationOption]
* @param {Object} obj
* @param {Object} obj.integrationOption - an integration option
* @param {String} obj.name
* @param {String} obj.type
* @param {String} obj.docsLink
* @returns
*/
const handleIntegrationOption = async ({
integrationOption,
accessToken
}: {
integrationOption: IntegrationOption,
accessToken?: string;
}) => {
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
if (!bot.isActive) {
await handleBotActivate();
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString('hex');
localStorage.setItem('latestCSRFToken', state);
let link = '';
switch (integrationOption.slug) {
case 'azure-key-vault':
link = `https://login.microsoftonline.com/${integrationOption.tenantId}/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case 'heroku':
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case 'vercel':
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case 'netlify':
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case 'github':
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/authorize`
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/authorize`
break;
default:
break;
}
if (integrationOption.type === 'oauth') {
// integration is of type OAuth
// 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;
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
}
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = '';
switch (integrationAuth.integration) {
case 'azure-key-vault':
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'heroku':
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'vercel':
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'netlify':
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'github':
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'render':
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case 'flyio':
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== '') {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
}
/**
* Open dialog to activate bot if bot is not active.
@@ -251,34 +251,20 @@ export default function Integrations() {
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
// consider: don't start integration until at [handleIntegrationOption] step
try {
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;
}
if (!bot.isActive) {
await handleBotActivate();
}
// case: integration has been authorized before
// -> create new integration
const integration = await createIntegration({
integrationAuthId: integrationAuthX._id
});
setIntegrations([...integrations, integration]);
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
@@ -360,12 +346,6 @@ export default function Integrations() {
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationAccessTokenDialog
isOpen={isIntegrationAccessTokenDialogOpen}
closeModal={() => setIntegrationAccessTokenDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
handleIntegrationOption={handleIntegrationOption}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
@@ -0,0 +1,124 @@
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,
Input,
Select,
SelectItem
} from '../../../components/v2';
import { useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
export default function AzureKeyVaultCreateIntegrationPage() {
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 [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
const [vaultBaseUrl, setVaultBaseUrl] = useState('');
const [vaultBaseUrlErrorText, setVaultBaseUrlErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
const handleButtonClick = async () => {
try {
if (vaultBaseUrl.length === 0) {
setVaultBaseUrlErrorText('Vault URI cannot be blank');
return;
}
if (
!vaultBaseUrl.startsWith('https://')
|| !vaultBaseUrl.endsWith('vault.azure.net')
) {
setVaultBaseUrlErrorText('Vault URI must be like https://<vault_name>.vault.azure.net');
return;
}
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: vaultBaseUrl,
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) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Azure Key Vault Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vault URI"
errorText={vaultBaseUrlErrorText}
isError={vaultBaseUrlErrorText !== '' ?? false}
>
<Input
placeholder='https://example.vault.azure.net'
value={vaultBaseUrl}
onChange={(e) => setVaultBaseUrl(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
AzureKeyVaultCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function AzureKeyVaultOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'azure-key-vault'
});
router.push(
`/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
AzureKeyVaultOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -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 FlyioCreateIntegrationPage() {
const router = useRouter();
const [accessToken, setAccessToken] = useState('');
const [accessTokenErrorText, setAccessTokenErrorText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setAccessTokenErrorText('');
if (accessToken.length === 0) {
setAccessTokenErrorText('Access token cannot be blank');
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem('projectData.id'),
integration: 'flyio',
accessToken
});
setIsLoading(false);
router.push(
`/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Fly.io Access Token"
errorText={accessTokenErrorText}
isError={accessTokenErrorText !== '' ?? false}
>
<Input
placeholder=''
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Fly.io
</Button>
</Card>
</div>
)
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -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 FlyioCreateIntegrationPage() {
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: 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) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Fly.io Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`flyio-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Fly.io App"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
FlyioCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,123 @@
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 GitHubCreateIntegrationPage() {
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 [owner, setOwner] = useState<string | null>(null);
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);
setOwner(integrationAuthApps[0]?.owner ?? null);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
owner
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>GitHub Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="GitHub Repo"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`github-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
GitHubCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function GitHubOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'github'
});
router.push(
`/integrations/github/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
GitHubOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,121 @@
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 HerokuCreateIntegrationPage() {
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 {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: 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) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Heroku Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Heroku App"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
HerokuCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,40 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function HerokuOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'heroku'
});
router.push(
`/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
HerokuOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,144 @@
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";
const netlifyEnvironments = [
{ name: 'Local development', slug: 'dev' },
{ name: 'Branch deploys', slug: 'branch-deploy' },
{ name: 'Deploy previews', slug: 'deploy-preview' },
{ name: 'Production', slug: 'production' }
]
export default function NetlifyCreateIntegrationPage() {
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 [targetEnvironment, setTargetEnvironment] = useState('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
setTargetEnvironment(netlifyEnvironments[0].slug);
}
}, [workspace]);
useEffect(() => {
// TODO: handle case where apps can be empty
if (integrationAuthApps) {
console.log(integrationAuthApps)
setTargetApp(integrationAuthApps[0].name);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
setIsLoading(true);
if (!integrationAuth?._id) return;
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Netlify Integration</CardTitle>
<FormControl
label="Project Environment"
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Netlify Site"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Netlify Context"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{netlifyEnvironments.map((netlifyEnvironment) => (
<SelectItem value={netlifyEnvironment.slug} key={`netlify-environment-${netlifyEnvironment.slug}`}>
{netlifyEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
NetlifyCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function NetlifyOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify'
});
router.push(
`/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
NetlifyOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -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 RenderCreateIntegrationPage() {
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: 'render',
accessToken: apiKey
});
setIsLoading(false);
router.push(
`/integrations/render/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
}
return (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Render API Key"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== '' ?? false}
>
<Input
placeholder='rnd_xxx'
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Connect to Render
</Button>
</Card>
</div>
)
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -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 RenderCreateIntegrationPage() {
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: 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) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Render Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Render Service"
className='mt-4'
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`render-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
RenderCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,142 @@
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";
const vercelEnvironments = [
{ name: 'Development', slug: 'development' },
{ name: 'Preview', slug: 'preview' },
{ name: 'Production', slug: 'production' }
]
export default function VercelCreateIntegrationPage() {
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 [targetEnvironment, setTargetEnvironemnt] = 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);
setTargetEnvironemnt(vercelEnvironments[0].slug);
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
owner: null
});
setIsLoading(false);
router.push(
`/integrations/${localStorage.getItem('projectData.id')}`
);
} catch (err) {
console.error(err);
}
}
return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
<div className="h-full w-full flex justify-center items-center">
<Card className="max-w-md p-8 rounded-md">
<CardTitle className='text-center'>Vercel Integration</CardTitle>
<FormControl
label="Project Environment"
className='mt-4'
>
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className='w-full border border-mineshaft-500'
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem value={sourceEnvironment.slug} key={`azure-key-vault-environment-${sourceEnvironment.slug}`}>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vercel App"
>
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className='w-full border border-mineshaft-500'
>
{integrationAuthApps.map((integrationAuthApp) => (
<SelectItem value={integrationAuthApp.name} key={`heroku-environment-${integrationAuthApp.name}`}>
{integrationAuthApp.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl
label="Vercel App Environment"
>
<Select
value={targetEnvironment}
onValueChange={(val) => setTargetEnvironemnt(val)}
className='w-full border border-mineshaft-500'
>
{vercelEnvironments.map((vercelEnvironment) => (
<SelectItem value={vercelEnvironment.slug} key={`vercel-environment-${vercelEnvironment.slug}`}>
{vercelEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className='mt-4'
isLoading={isLoading}
>
Create Integration
</Button>
</Card>
</div>
) : <div />
}
VercelCreateIntegrationPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
@@ -0,0 +1,41 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../../components/utilities/withTranslateProps';
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
export default function VercelOAuth2CallbackPage() {
const router = useRouter();
const { code, state } = queryString.parse(router.asPath.split('?')[1]);
useEffect(() => {
(async () => {
try {
// validate state
if (state !== localStorage.getItem('latestCSRFToken')) return;
localStorage.removeItem('latestCSRFToken');
const integrationAuth = await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel'
});
router.push(
`/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`
);
} catch (err) {
console.error(err);
}
})();
}, []);
return <div />
}
VercelOAuth2CallbackPage.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Netlify() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
// modify comment here
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'netlify',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Netlify integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Netlify.requireAuth = true;
-46
View File
@@ -1,46 +0,0 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import queryString from 'query-string';
import AuthorizeIntegration from './api/integrations/authorizeIntegration';
export default function Vercel() {
const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
const {code} = parsedUrl;
const {state} = parsedUrl;
/**
* Here we forward to the default workspace if a user opens this url
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
try {
// type check
if (!code) throw new Error('Code not found');
if (state === localStorage.getItem('latestCSRFToken')) {
localStorage.removeItem('latestCSRFToken');
await AuthorizeIntegration({
workspaceId: localStorage.getItem('projectData.id') as string,
code: code as string,
integration: 'vercel',
});
router.push(
`/integrations/${ localStorage.getItem('projectData.id')}`
);
}
} catch (err) {
console.error('Vercel integration error: ', err);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <div />;
}
Vercel.requireAuth = true;