feat(#31): implemented ui for multi env and integrated api with backend

fix(#31): fixed all v2 release conflict
This commit is contained in:
akhilmhdh
2023-01-11 23:12:05 +05:30
parent 9116bf3344
commit 84700308f5
23 changed files with 1051 additions and 851 deletions
+1 -1
View File
@@ -104,12 +104,12 @@ app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes // v2 routes
app.use('/api/v2/workspace', v2EnvironmentRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
app.use('/api/v2/secrets', v2SecretsRouter); app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/api-key-data', v2APIKeyDataRouter); app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
app.use('/api/v2/environments', v2EnvironmentRouter);
// api docs // api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node';
import axios from 'axios'; import axios from 'axios';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { IntegrationAuth, Integration } from '../../models'; import { IntegrationAuth, Integration } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables'; import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
import { IntegrationService } from '../../services'; import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../../integrations'; import { getApps, revokeAccess } from '../../integrations';
@@ -1,6 +1,13 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Secret, ServiceToken, Workspace, Integration } from '../../models'; import {
Secret,
ServiceToken,
Workspace,
Integration,
ServiceTokenData,
} from '../../models';
import { SecretVersion } from '../../ee/models';
/** /**
* Create new workspace environment named [environmentName] under workspace with id * Create new workspace environment named [environmentName] under workspace with id
@@ -12,25 +19,24 @@ export const createWorkspaceEnvironment = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
const { workspaceId, environmentName, environmentSlug } = req.body; const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
try { try {
// atomic create the environment const workspace = await Workspace.findById(workspaceId).exec();
const workspace = await Workspace.findOneAndUpdate( if (
{ !workspace ||
_id: workspaceId, workspace?.environments.find(
'environments.slug': { $ne: environmentSlug }, ({ name, slug }) => slug === environmentSlug || environmentName === name
'environments.name': { $ne: environmentName }, )
}, ) {
{ throw new Error('Failed to create workspace environment');
$addToSet: {
environments: { name: environmentName, slug: environmentSlug },
},
}
);
if (!workspace) {
throw new Error('Failed to update workspace environment');
} }
workspace?.environments.push({
name: environmentName.toLowerCase(),
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
@@ -60,8 +66,8 @@ export const renameWorkspaceEnvironment = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
const { workspaceId, environmentName, environmentSlug, oldEnvironmentSlug } = const { workspaceId } = req.params;
req.body; const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try { try {
// user should pass both new slug and env name // user should pass both new slug and env name
if (!environmentSlug || !environmentName) { if (!environmentSlug || !environmentName) {
@@ -69,25 +75,47 @@ export const renameWorkspaceEnvironment = async (
} }
// atomic update the env to avoid conflict // atomic update the env to avoid conflict
const workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findById(workspaceId).exec();
{ _id: workspaceId, 'environments.slug': oldEnvironmentSlug },
{
'environments.$.name': environmentName,
'environments.$.slug': environmentSlug,
}
);
if (!workspace) { if (!workspace) {
throw new Error('Failed to update workspace'); throw new Error('Failed to create workspace environment');
} }
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName.toLowerCase();
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany( await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { environment: environmentSlug }
); );
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany( await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { environment: environmentSlug }
); );
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany( await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { environment: environmentSlug }
@@ -120,37 +148,46 @@ export const deleteWorkspaceEnvironment = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
const { workspaceId, environmentSlug } = req.body; const { workspaceId } = req.params;
const { environmentSlug } = req.body;
try { try {
// atomic delete the env in the workspacce // atomic update the env to avoid conflict
const workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findById(workspaceId).exec();
{ _id: workspaceId },
{
$pull: {
environments: {
slug: environmentSlug,
},
},
}
);
if (!workspace) { if (!workspace) {
throw new Error('Failed to delete workspace environment'); throw new Error('Failed to create workspace environment');
} }
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up // clean up
await Secret.deleteMany({ await Secret.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({ await ServiceToken.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({ await Integration.deleteMany({
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug, environment: environmentSlug,
}); });
} catch (err) { } catch (err) {
Sentry.setUser({ email: req.user.email }); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); Sentry.captureException(err);
-5
View File
@@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
import { import {
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables'; } from '../../variables';
export interface ISecretVersion { export interface ISecretVersion {
@@ -56,7 +52,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
}, },
environment: { environment: {
type: String, type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true required: true
}, },
isDeleted: { // consider removing field isDeleted: { // consider removing field
+1 -1
View File
@@ -9,7 +9,7 @@ import {
export interface IIntegration { export interface IIntegration {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: 'dev' | 'test' | 'staging' | 'prod'; environment: string;
isActive: boolean; isActive: boolean;
app: string; app: string;
target: string; target: string;
+14 -11
View File
@@ -1,4 +1,4 @@
import express from 'express'; import express, { Response, Request } from 'express';
const router = express.Router(); const router = express.Router();
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { environmentController } from '../../controllers/v2'; import { environmentController } from '../../controllers/v2';
@@ -7,14 +7,15 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest, validateRequest,
} from '../../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; import { ADMIN, MEMBER } from '../../variables';
router.post( router.post(
'/:workspaceId', '/:workspaceId/environments',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED],
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -24,11 +25,12 @@ router.post(
); );
router.put( router.put(
'/:workspaceId', '/:workspaceId/environments',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED],
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
@@ -39,11 +41,12 @@ router.put(
); );
router.delete( router.delete(
'/:workspaceId', '/:workspaceId/environments',
requireAuth, requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN], acceptedRoles: [ADMIN],
acceptedStatuses: [GRANTED],
}), }),
param('workspaceId').exists().trim(), param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(), body('environmentSlug').exists().trim(),
+2 -2
View File
@@ -18,7 +18,7 @@ import {
router.post( router.post(
'/', '/',
body('workspaceId').exists().isString().trim(), body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim().isIn(['dev', 'staging', 'prod', 'test']), body('environment').exists().isString().trim(),
body('secrets') body('secrets')
.exists() .exists()
.custom((value) => { .custom((value) => {
@@ -73,7 +73,7 @@ router.post(
router.get( router.get(
'/', '/',
query('workspaceId').exists().trim(), query('workspaceId').exists().trim(),
query('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']), query('environment').exists().trim(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'serviceToken'] acceptedAuthModes: ['jwt', 'serviceToken']
+1 -1
View File
@@ -69,7 +69,7 @@ export default function ListBox({
<Listbox.Option <Listbox.Option
key={personIdx} key={personIdx}
className={({ active, selected }) => className={({ active, selected }) =>
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${ `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
selected ? 'bg-white/10 text-gray-400 font-bold' : '' selected ? 'bg-white/10 text-gray-400 font-bold' : ''
} ${ } ${
active && !selected active && !selected
@@ -8,7 +8,6 @@ import nacl from "tweetnacl";
import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { envMapping } from "../../../public/data/frequentConstants";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric, encryptAssymmetric,
@@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({
workspaceId, workspaceId,
workspaceName, workspaceName,
serviceTokens, serviceTokens,
environments,
setServiceTokens setServiceTokens
}) => { }) => {
const [serviceToken, setServiceToken] = useState(""); const [serviceToken, setServiceToken] = useState("");
const [serviceTokenName, setServiceTokenName] = useState(""); const [serviceTokenName, setServiceTokenName] = useState("");
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
const [serviceTokenCopied, setServiceTokenCopied] = useState(false); const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -66,7 +66,7 @@ const AddServiceTokenDialog = ({
let newServiceToken = await addServiceToken({ let newServiceToken = await addServiceToken({
name: serviceTokenName, name: serviceTokenName,
workspaceId, workspaceId,
environment: envMapping[serviceTokenEnv], environment: selectedServiceTokenEnv.slug,
expiresIn: expiryMapping[serviceTokenExpiresIn], expiresIn: expiryMapping[serviceTokenExpiresIn],
encryptedKey: ciphertext, encryptedKey: ciphertext,
iv, iv,
@@ -101,155 +101,159 @@ const AddServiceTokenDialog = ({
}; };
return ( return (
<div className="z-50"> <div className='z-50'>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}> <Dialog as='div' className='relative' onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter='ease-out duration-300'
enterFrom="opacity-0" enterFrom='opacity-0'
enterTo="opacity-100" enterTo='opacity-100'
leave="ease-in duration-200" leave='ease-in duration-200'
leaveFrom="opacity-100" leaveFrom='opacity-100'
leaveTo="opacity-0" leaveTo='opacity-0'
> >
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" /> <div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 overflow-y-auto"> <div className='fixed inset-0 overflow-y-auto'>
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter='ease-out duration-300'
enterFrom="opacity-0 scale-95" enterFrom='opacity-0 scale-95'
enterTo="opacity-100 scale-100" enterTo='opacity-100 scale-100'
leave="ease-in duration-200" leave='ease-in duration-200'
leaveFrom="opacity-100 scale-100" leaveFrom='opacity-100 scale-100'
leaveTo="opacity-0 scale-95" leaveTo='opacity-0 scale-95'
> >
{serviceToken == "" ? ( {serviceToken == '' ? (
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title <Dialog.Title
as="h3" as='h3'
className="text-lg font-medium leading-6 text-gray-400 z-50" className='text-lg font-medium leading-6 text-gray-400 z-50'
> >
{t("section-token:add-dialog.title", { {t('section-token:add-dialog.title', {
target: workspaceName, target: workspaceName,
})} })}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-4"> <div className='mt-2 mb-4'>
<div className="flex flex-col"> <div className='flex flex-col'>
<p className="text-sm text-gray-500"> <p className='text-sm text-gray-500'>
{t("section-token:add-dialog.description")} {t('section-token:add-dialog.description')}
</p> </p>
</div> </div>
</div> </div>
<div className="max-h-28 mb-2"> <div className='max-h-28 mb-2'>
<InputField <InputField
label={t("section-token:add-dialog.name")} label={t('section-token:add-dialog.name')}
onChangeHandler={setServiceTokenName} onChangeHandler={setServiceTokenName}
type="varName" type='varName'
value={serviceTokenName} value={serviceTokenName}
placeholder="" placeholder=''
isRequired isRequired
/> />
</div> </div>
<div className="max-h-28 mb-2"> <div className='max-h-28 mb-2'>
<ListBox <ListBox
selected={serviceTokenEnv} selected={selectedServiceTokenEnv?.name}
onChange={setServiceTokenEnv} data={environments.map(({ name }) => name)}
data={[ onChange={(envName) =>
"Development", setSelectedServiceTokenEnv(
"Staging", environments.find(
"Production", ({ name }) => envName === name
"Testing", ) || {
]} name: 'unknown',
slug: 'unknown',
}
)
}
isFull={true} isFull={true}
text={`${t("common:environment")}: `} text={`${t('common:environment')}: `}
/> />
</div> </div>
<div className="max-h-28"> <div className='max-h-28'>
<ListBox <ListBox
selected={serviceTokenExpiresIn} selected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn} onChange={setServiceTokenExpiresIn}
data={[ data={[
"1 day", '1 day',
"7 days", '7 days',
"1 month", '1 month',
"6 months", '6 months',
"12 months", '12 months',
]} ]}
isFull={true} isFull={true}
text={`${t("common:expired-in")}: `} text={`${t('common:expired-in')}: `}
/> />
</div> </div>
<div className="max-w-max"> <div className='max-w-max'>
<div className="mt-6 flex flex-col justify-start w-max"> <div className='mt-6 flex flex-col justify-start w-max'>
<Button <Button
onButtonPressed={() => generateServiceToken()} onButtonPressed={() => generateServiceToken()}
color="mineshaft" color='mineshaft'
text={t("section-token:add-dialog.add")} text={t('section-token:add-dialog.add')}
textDisabled={t("section-token:add-dialog.add")} textDisabled={t('section-token:add-dialog.add')}
size="md" size='md'
active={serviceTokenName == "" ? false : true} active={serviceTokenName == '' ? false : true}
/> />
</div> </div>
</div> </div>
</Dialog.Panel> </Dialog.Panel>
) : ( ) : (
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title <Dialog.Title
as="h3" as='h3'
className="text-lg font-medium leading-6 text-gray-400 z-50" className='text-lg font-medium leading-6 text-gray-400 z-50'
> >
{t("section-token:add-dialog.copy-service-token")} {t('section-token:add-dialog.copy-service-token')}
</Dialog.Title> </Dialog.Title>
<div className="mt-2 mb-4"> <div className='mt-2 mb-4'>
<div className="flex flex-col"> <div className='flex flex-col'>
<p className="text-sm text-gray-500"> <p className='text-sm text-gray-500'>
{t( {t(
"section-token:add-dialog.copy-service-token-description" 'section-token:add-dialog.copy-service-token-description'
)} )}
</p> </p>
</div> </div>
</div> </div>
<div className="w-full"> <div className='w-full'>
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20"> <div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
<input <input
type="text" type='text'
value={serviceToken} value={serviceToken}
id="serviceToken" id='serviceToken'
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none" className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
></input> ></input>
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none"> <div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
{serviceToken} {serviceToken}
</div> </div>
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200"> <div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button <button
onClick={copyToClipboard} onClick={copyToClipboard}
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200" className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
> >
{serviceTokenCopied ? ( {serviceTokenCopied ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheck} icon={faCheck}
className="pr-0.5" className='pr-0.5'
/> />
) : ( ) : (
<FontAwesomeIcon icon={faCopy} /> <FontAwesomeIcon icon={faCopy} />
)} )}
</button> </button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm"> <span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
{t("common:click-to-copy")} {t('common:click-to-copy')}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<div className="mt-6 flex flex-col justify-start w-max"> <div className='mt-6 flex flex-col justify-start w-max'>
<Button <Button
onButtonPressed={() => closeAddServiceTokenModal()} onButtonPressed={() => closeAddServiceTokenModal()}
color="mineshaft" color='mineshaft'
text="Close" text='Close'
size="md" size='md'
/> />
</div> </div>
</Dialog.Panel> </Dialog.Panel>
@@ -12,15 +12,24 @@ type Props = {
// on edit mode load up initial values // on edit mode load up initial values
initialValues?: FormFields; initialValues?: FormFields;
onClose: () => void; onClose: () => void;
onSubmit: (envName: string, envSlug: string) => void; onCreateSubmit: (data: FormFields) => void;
onEditSubmit: (data: FormFields) => void;
}; };
// TODO: Migrate to better form management and validation. Preferable react-hook-form + yup
/** /**
* The dialog modal for when the user wants to create a new workspace * The dialog modal for when the user wants to create a new workspace
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues, isEditMode }: Props) => { export const AddUpdateEnvironmentDialog = ({
isOpen,
onClose,
onCreateSubmit,
onEditSubmit,
initialValues,
isEditMode,
}: Props) => {
const [formInput, setFormInput] = useState<FormFields>({ const [formInput, setFormInput] = useState<FormFields>({
name: '', name: '',
slug: '', slug: '',
@@ -39,7 +48,15 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues,
const onFormSubmit: FormEventHandler = (e) => { const onFormSubmit: FormEventHandler = (e) => {
e.preventDefault(); e.preventDefault();
console.log(formInput); const data = {
name: formInput.name.toLowerCase(),
slug: formInput.slug.toLowerCase(),
};
if (isEditMode) {
onEditSubmit(data);
return;
}
onCreateSubmit(data);
}; };
return ( return (
@@ -81,7 +98,7 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues,
<form onSubmit={onFormSubmit}> <form onSubmit={onFormSubmit}>
<div className='max-h-28 mt-4'> <div className='max-h-28 mt-4'>
<InputField <InputField
label='Project Name' label='Environment Name'
onChangeHandler={(val) => onInputChange('name', val)} onChangeHandler={(val) => onInputChange('name', val)}
type='varName' type='varName'
value={formInput.name} value={formInput.name}
@@ -112,6 +129,7 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues,
type='submit' type='submit'
color='mineshaft' color='mineshaft'
text={isEditMode ? 'Update' : 'Create'} text={isEditMode ? 'Update' : 'Create'}
active={formInput.name !== '' && formInput.slug !== ''}
size='md' size='md'
/> />
</div> </div>
@@ -1,4 +1,4 @@
import { Fragment, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import InputField from '../InputField'; import InputField from '../InputField';
@@ -21,6 +21,10 @@ const DeleteActionModal = ({
}:Props) => { }:Props) => {
const [deleteInputField, setDeleteInputField] = useState("") const [deleteInputField, setDeleteInputField] = useState("")
useEffect(() => {
setDeleteInputField("");
}, [isOpen]);
return ( return (
<div> <div>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
@@ -1,15 +1,61 @@
import { faPencil,faPlus,faX } from '@fortawesome/free-solid-svg-icons'; import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
import { usePopUp } from '../../../hooks/usePopUp'; import { usePopUp } from '../../../hooks/usePopUp';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
import {AddEnvironmentDialog} from '../dialog/AddEnvironmentDialog'; import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog';
import DeleteActionModal from '../dialog/DeleteActionModal'; import DeleteActionModal from '../dialog/DeleteActionModal';
const EnvironmentTable = ({ data = [] }) => { type Env = { name: string; slug: string };
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'createUpdateEnv', type Props = {
'deleteEnv', data: Env[];
] as const); onCreateEnv: (arg0: Env) => Promise<void>;
onUpdateEnv: (oldSlug: string, arg0: Env) => Promise<void>;
onDeleteEnv: (slug: string) => Promise<void>;
};
const EnvironmentTable = ({
data = [],
onCreateEnv,
onDeleteEnv,
onUpdateEnv,
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'createUpdateEnv',
'deleteEnv',
] as const);
const onEnvCreateCB = async (env: Env) => {
try {
await onCreateEnv(env);
handlePopUpClose('createUpdateEnv');
} catch (error) {
console.error(error);
}
};
const onEnvUpdateCB = async (env: Env) => {
try {
await onUpdateEnv(
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
env
);
handlePopUpClose('createUpdateEnv');
} catch (error) {
console.error(error);
}
};
const onEnvDeleteCB = async () => {
try {
await onDeleteEnv(
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
);
handlePopUpClose('deleteEnv');
} catch (error) {
console.error(error);
}
};
return ( return (
<> <>
@@ -62,7 +108,9 @@ const EnvironmentTable = ({ data = [] }) => {
<td className='py-2 border-mineshaft-700 border-t flex'> <td className='py-2 border-mineshaft-700 border-t flex'>
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'> <div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
<Button <Button
onButtonPressed={() => handlePopUpOpen("createUpdateEnv",{ name, slug })} onButtonPressed={() =>
handlePopUpOpen('createUpdateEnv', { name, slug })
}
color='red' color='red'
size='icon-sm' size='icon-sm'
icon={faPencil} icon={faPencil}
@@ -101,14 +149,15 @@ const EnvironmentTable = ({ data = [] }) => {
}?`} }?`}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''} deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
onClose={() => handlePopUpClose('deleteEnv')} onClose={() => handlePopUpClose('deleteEnv')}
onSubmit={() => handlePopUpClose('deleteEnv')} onSubmit={onEnvDeleteCB}
/> />
<AddEnvironmentDialog <AddUpdateEnvironmentDialog
isOpen={popUp.createUpdateEnv.isOpen} isOpen={popUp.createUpdateEnv.isOpen}
isEditMode={Boolean(popUp.createUpdateEnv?.data)} isEditMode={Boolean(popUp.createUpdateEnv?.data)}
initialValues={popUp?.createUpdateEnv?.data as any} initialValues={popUp?.createUpdateEnv?.data as any}
onClose={() => handlePopUpClose('createUpdateEnv')} onClose={() => handlePopUpClose('createUpdateEnv')}
onSubmit={() => null} onCreateSubmit={onEnvCreateCB}
onEditSubmit={onEnvUpdateCB}
/> />
</div> </div>
</> </>
@@ -3,7 +3,6 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../../utilities/randomId'; import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
@@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
{workspaceName} {workspaceName}
</td> </td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{reverseEnvMapping[row.environment]} {row.environment}
</td> </td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()} {new Date(row.expiresAt).toUTCString()}
@@ -15,9 +15,7 @@ import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"
import updateIntegration from "../../pages/api/integrations/updateIntegration" import updateIntegration from "../../pages/api/integrations/updateIntegration"
import { import {
contextNetlifyMapping, contextNetlifyMapping,
envMapping,
reverseContextNetlifyMapping, reverseContextNetlifyMapping,
reverseEnvMapping,
} from "../../public/data/frequentConstants"; } from "../../public/data/frequentConstants";
interface Integration { interface Integration {
@@ -41,9 +39,7 @@ const Integration = ({
}: { }: {
integration: Integration; integration: Integration;
}) => { }) => {
const [integrationEnvironment, setIntegrationEnvironment] = useState( const [integrationEnvironment, setIntegrationEnvironment] = useState(integration.environment);
reverseEnvMapping[integration.environment]
);
const [fileState, setFileState] = useState([]); const [fileState, setFileState] = useState([]);
const router = useRouter(); const router = useRouter();
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
@@ -199,9 +195,9 @@ const Integration = ({
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const siteId = siteApp?.siteId ? siteApp.siteId : null; const siteId = siteApp?.siteId ? siteApp.siteId : null;
const result = await updateIntegration({ await updateIntegration({
integrationId: integration._id, integrationId: integration._id,
environment: envMapping[integrationEnvironment], environment: integrationEnvironment,
app: integrationApp, app: integrationApp,
isActive: true, isActive: true,
target: integrationTarget ? integrationTarget.toLowerCase() : null, target: integrationTarget ? integrationTarget.toLowerCase() : null,
@@ -1,4 +1,3 @@
import { envMapping } from "../../../public/data/frequentConstants";
import checkOverrides from './checkOverrides'; import checkOverrides from './checkOverrides';
@@ -39,7 +38,7 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
const fileDownloadUrl = URL.createObjectURL(blob); const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a'); const alink = document.createElement('a');
alink.href = fileDownloadUrl; alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env'; alink.download = env + '.env';
alink.click(); alink.click();
} }
@@ -1,8 +1,6 @@
import getSecrets from '~/pages/api/files/GetSecrets'; import getSecrets from '~/pages/api/files/GetSecrets';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import { envMapping } from '../../../public/data/frequentConstants';
const { const {
decryptAssymmetric, decryptAssymmetric,
decryptSymmetric decryptSymmetric
@@ -35,7 +33,7 @@ interface SecretProps {
} }
interface FunctionProps { interface FunctionProps {
env: keyof typeof envMapping; env: string;
setIsKeyAvailable: any; setIsKeyAvailable: any;
setData: any; setData: any;
workspaceId: string; workspaceId: string;
@@ -58,7 +56,7 @@ const getSecretsForProject = async ({
try { try {
let encryptedSecrets; let encryptedSecrets;
try { try {
encryptedSecrets = await getSecrets(workspaceId, envMapping[env]); encryptedSecrets = await getSecrets(workspaceId, env);
} catch (error) { } catch (error) {
console.log('ERROR: Not able to access the latest version of secrets'); console.log('ERROR: Not able to access the latest version of secrets');
} }
@@ -0,0 +1,29 @@
import SecurityClient from '~/utilities/SecurityClient';
type NewEnvironmentInfo = {
environmentSlug: string;
environmentName: string;
};
/**
* This route deletes a specified workspace.
* @param {*} workspaceId
* @returns
*/
const createEnvironment = (workspaceId:string, newEnv: NewEnvironmentInfo) => {
return SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newEnv)
}).then(async (res) => {
if (res && res.status == 200) {
return res;
} else {
console.log('Failed to create environment');
}
});
};
export default createEnvironment;
@@ -0,0 +1,26 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This route deletes a specified env.
* @param {*} workspaceId
* @returns
*/
const deleteEnvironment = (workspaceId: string, environmentSlug: string) => {
return SecurityClient.fetchCall(
`/api/v2/workspace/${workspaceId}/environments`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ environmentSlug }),
}
).then(async (res) => {
if (res && res.status == 200) {
return res;
} else {
console.log('Failed to delete environment');
}
});
};
export default deleteEnvironment;
@@ -0,0 +1,33 @@
import SecurityClient from '~/utilities/SecurityClient';
type EnvironmentInfo = {
oldEnvironmentSlug: string;
environmentSlug: string;
environmentName: string;
};
/**
* This route updates a specified environment.
* @param {*} workspaceId
* @returns
*/
const updateEnvironment = (workspaceId: string, env: EnvironmentInfo) => {
return SecurityClient.fetchCall(
`/api/v2/workspace/${workspaceId}/environments`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(env),
}
).then(async (res) => {
if (res && res.status == 200) {
return res;
} else {
console.log('Failed to update environment');
}
});
};
export default updateEnvironment;
@@ -5,6 +5,7 @@ interface Workspace {
_id: string; _id: string;
name: string; name: string;
organization: string; organization: string;
environments: Array<{name:string, slug:string}>
} }
/** /**
File diff suppressed because it is too large Load Diff
-306
View File
@@ -1,306 +0,0 @@
import { useEffect, useRef, useState } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { faCheck, faCopy, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from "~/components/basic/buttons/Button";
import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog";
import InputField from "~/components/basic/InputField";
import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable.tsx";
import NavHeader from "~/components/navigation/NavHeader";
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
import getServiceTokens from "../../api/serviceToken/getServiceTokens";
import deleteWorkspace from "../../api/workspace/deleteWorkspace";
import getWorkspaces from "../../api/workspace/getWorkspaces";
import renameWorkspace from "../../api/workspace/renameWorkspace";
export default function SettingsBasic() {
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [workspaceName, setWorkspaceName] = useState("");
const [serviceTokens, setServiceTokens] = useState([]);
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState("");
const [workspaceId, setWorkspaceId] = useState("");
const [isAddOpen, setIsAddOpen] = useState(false);
let [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] =
useState(false);
const [projectIdCopied, setProjectIdCopied] = useState(false);
const { t } = useTranslation();
/**
* This function copies the project id to the clipboard
*/
function copyToClipboard() {
// const copyText = document.getElementById('myInput') as HTMLInputElement;
const copyText = document.getElementById('myInput')
if (copyText) {
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(copyText.value);
setProjectIdCopied(true);
setTimeout(() => setProjectIdCopied(false), 2000);
}
}
useEffect(async () => {
let userWorkspaces = await getWorkspaces();
userWorkspaces.map((userWorkspace) => {
if (userWorkspace._id == router.query.id) {
setWorkspaceName(userWorkspace.name);
}
});
let tempServiceTokens = await getServiceTokens({
workspaceId: router.query.id,
});
setServiceTokens(tempServiceTokens);
}, []);
const modifyWorkspaceName = (newName) => {
setButtonReady(true);
setWorkspaceName(newName);
};
const submitChanges = (newWorkspaceName) => {
renameWorkspace(router.query.id, newWorkspaceName);
setButtonReady(false);
};
useEffect(async () => {
setWorkspaceId(router.query.id);
}, []);
function closeAddModal() {
setIsAddOpen(false);
}
function openAddModal() {
setIsAddOpen(true);
}
const closeAddServiceTokenModal = () => {
setIsAddServiceTokenDialogOpen(false);
};
/**
* This function deleted a workspace.
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another aviable workspace.
*/
const executeDeletingWorkspace = async () => {
let userWorkspaces = await getWorkspaces();
if (userWorkspaces.length > 1) {
if (
userWorkspaces.filter(
(workspace) => workspace._id == router.query.id
)[0].name == workspaceToBeDeletedName
) {
await deleteWorkspace(router.query.id);
let userWorkspaces = await getWorkspaces();
router.push("/dashboard/" + userWorkspaces[0]._id);
}
}
};
return (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<Head>
<title>
{t("common:head-title", { title: t("settings-project:title") })}
</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<AddServiceTokenDialog
isOpen={isAddServiceTokenDialogOpen}
workspaceId={router.query.id}
closeModal={closeAddServiceTokenModal}
workspaceName={workspaceName}
serviceTokens={serviceTokens}
setServiceTokens={setServiceTokens}
/>
<div className="flex flex-row mr-6 max-w-5xl">
<div className="w-full max-h-screen pb-2 overflow-y-auto">
<NavHeader
pageName={t("settings-project:title")}
isProjectRelated={true}
/>
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
<div className="flex flex-col justify-start items-start text-3xl">
<p className="font-semibold mr-4 text-gray-200">
{t("settings-project:title")}
</p>
<p className="font-normal mr-4 text-gray-400 text-base">
{t("settings-project:description")}
</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50">
<div className="flex flex-col">
<div className="min-w-md mt-2 flex flex-col items-start">
<div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2">
<p className="text-xl font-semibold mb-4 mt-2">
{t("common:display-name")}
</p>
<div className="max-h-28 w-full max-w-md mr-auto">
<InputField
onChangeHandler={modifyWorkspaceName}
type="varName"
value={workspaceName}
placeholder=""
isRequired
/>
</div>
<div className="flex justify-start w-full">
<div className={`flex justify-start max-w-sm mt-4 mb-2`}>
<Button
text={t("common:save-changes")}
onButtonPressed={() => submitChanges(workspaceName)}
color="mineshaft"
size="md"
active={buttonReady}
iconDisabled={faCheck}
textDisabled="Saved"
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
<p className="text-xl font-semibold self-start">
{t("common:project-id")}
</p>
<p className="text-base text-gray-400 font-normal self-start mt-4">
{t("settings-project:project-id-description")}
</p>
<p className="text-base text-gray-400 font-normal self-start">
{t("settings-project:project-id-description2")}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a
href="https://infisical.com/docs/getting-started/introduction"
target="_blank"
rel="noopener"
className="text-primary hover:opacity-80 duration-200"
>
{t("settings-project:docs")}
</a>
</p>
<p className="mt-4 text-xs text-bunker-300">{t("settings-project:auto-generated")}</p>
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400">
<p className="mr-2 font-bold pl-4">{`${t(
"common:project-id"
)}:`}</p>
<input
type="text"
value={workspaceId}
id="myInput"
className="bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none"
disabled
></input>
<div className="group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200">
<button
onClick={copyToClipboard}
className="pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
>
{projectIdCopied ? (
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm">
{t("common:click-to-copy")}
</span>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
<p className="text-xl font-semibold mb-3">
{t("section-token:service-tokens")}
</p>
<p className="text-sm text-gray-400">
{t("section-token:service-tokens-description")}
</p>
<p className="text-sm text-gray-400 mb-4">
Please, make sure you are on the
<a
className="text-primary underline underline-offset-2 ml-1"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noreferrer"
>
latest version of CLI
</a>.
</p>
</div>
<div className="w-48 mt-2">
<Button
text={t("section-token:add-new")}
onButtonPressed={() => {
setIsAddServiceTokenDialogOpen(true);
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<ServiceTokenTable
data={serviceTokens}
workspaceName={workspaceName}
setServiceTokens={setServiceTokens}
/>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-4 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
<p className="text-xl font-bold text-red">
{t("settings-project:danger-zone")}
</p>
<p className="mt-2 text-md text-gray-400">
{t("settings-project:danger-zone-note")}
</p>
<div className="max-h-28 w-full max-w-md mr-auto mt-4">
<InputField
label={t("settings-project:project-to-delete")}
onChangeHandler={setWorkspaceToBeDeletedName}
type="varName"
value={workspaceToBeDeletedName}
placeholder=""
isRequired
/>
</div>
<button
type="button"
className="max-w-md mt-6 w-full inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-semibold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={executeDeletingWorkspace}
>
{t("settings-project:delete-project")}
</button>
<p className="mt-0.5 ml-1 text-xs text-gray-500">
{t("settings-project:delete-project-note")}
</p>
</div>
</div>
</div>
</div>
</div>
);
}
SettingsBasic.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([
"settings",
"settings-project",
"section-token",
]);
+173 -61
View File
@@ -1,55 +1,87 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from 'react';
import Head from "next/head"; import Head from 'next/head';
import { useRouter } from "next/router"; import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faCopy, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from "~/components/basic/buttons/Button"; import Button from '~/components/basic/buttons/Button';
import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog"; import AddServiceTokenDialog from '~/components/basic/dialog/AddServiceTokenDialog';
import InputField from "~/components/basic/InputField"; import InputField from '~/components/basic/InputField';
import EnvironmentTable from '~/components/basic/table/EnvironmentsTable'; import EnvironmentTable from '~/components/basic/table/EnvironmentsTable';
import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable"; import ServiceTokenTable from '~/components/basic/table/ServiceTokenTable';
import NavHeader from "~/components/navigation/NavHeader"; import NavHeader from '~/components/navigation/NavHeader';
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; import deleteEnvironment from '~/pages/api/environments/deleteEnvironment';
import updateEnvironment from '~/pages/api/environments/updateEnvironment';
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
import getServiceTokens from "../../api/serviceToken/getServiceTokens"; import createEnvironment from '../../api/environments/createEnvironment';
import deleteWorkspace from "../../api/workspace/deleteWorkspace"; import getServiceTokens from '../../api/serviceToken/getServiceTokens';
import getWorkspaces from "../../api/workspace/getWorkspaces"; import deleteWorkspace from '../../api/workspace/deleteWorkspace';
import renameWorkspace from "../../api/workspace/renameWorkspace"; import getWorkspaces from '../../api/workspace/getWorkspaces';
import renameWorkspace from '../../api/workspace/renameWorkspace';
type EnvData = {
name: string;
slug: string;
};
export default function SettingsBasic() { export default function SettingsBasic() {
const [buttonReady, setButtonReady] = useState(false); const [buttonReady, setButtonReady] = useState(false);
const router = useRouter(); const router = useRouter();
const [workspaceName, setWorkspaceName] = useState(""); const [workspaceName, setWorkspaceName] = useState('');
const [serviceTokens, setServiceTokens] = useState([]); const [serviceTokens, setServiceTokens] = useState([]);
const [environments,setEnvironments] = useState([]); const [environments, setEnvironments] = useState<Array<EnvData>>([]);
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(""); const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
const [isAddOpen, setIsAddOpen] = useState(false); const [isAddOpen, setIsAddOpen] = useState(false);
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false); const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] =
useState(false);
const [projectIdCopied, setProjectIdCopied] = useState(false);
const workspaceId = router.query.id as string;
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(async () => { /**
const userWorkspaces = await getWorkspaces(); * This function copies the project id to the clipboard
userWorkspaces.forEach((userWorkspace) => { */
if (userWorkspace._id == router.query.id) { function copyToClipboard() {
setWorkspaceName(userWorkspace.name); const copyText = document.getElementById('myInput') as HTMLInputElement;
setEnvironments(userWorkspace.environments);
} if (copyText) {
}); copyText.select();
const tempServiceTokens = await getServiceTokens({ copyText.setSelectionRange(0, 99999); // For mobile devices
workspaceId: router.query.id,
}); navigator.clipboard.writeText(copyText.value);
setServiceTokens(tempServiceTokens);
setProjectIdCopied(true);
setTimeout(() => setProjectIdCopied(false), 2000);
}
}
useEffect(() => {
const load = async () => {
const userWorkspaces = await getWorkspaces();
userWorkspaces.forEach((userWorkspace) => {
if (userWorkspace._id == workspaceId) {
setWorkspaceName(userWorkspace.name);
setEnvironments(userWorkspace.environments);
}
});
const tempServiceTokens = await getServiceTokens({
workspaceId,
});
setServiceTokens(tempServiceTokens);
};
load();
}, []); }, []);
const modifyWorkspaceName = (newName) => { const modifyWorkspaceName = (newName: string) => {
setButtonReady(true); setButtonReady(true);
setWorkspaceName(newName); setWorkspaceName(newName);
}; };
const submitChanges = (newWorkspaceName) => { const submitChanges = (newWorkspaceName: string) => {
renameWorkspace(router.query.id, newWorkspaceName); renameWorkspace(workspaceId, newWorkspaceName);
setButtonReady(false); setButtonReady(false);
}; };
@@ -69,16 +101,54 @@ export default function SettingsBasic() {
if (userWorkspaces.length > 1) { if (userWorkspaces.length > 1) {
if ( if (
userWorkspaces.filter( userWorkspaces.filter(
(workspace) => workspace._id == router.query.id (workspace) => workspace._id === workspaceId
)[0].name == workspaceToBeDeletedName )[0].name == workspaceToBeDeletedName
) { ) {
await deleteWorkspace(router.query.id); await deleteWorkspace(workspaceId);
const userWorkspaces = await getWorkspaces(); const userWorkspaces = await getWorkspaces();
router.push("/dashboard/" + userWorkspaces[0]._id); router.push('/dashboard/' + userWorkspaces[0]._id);
} }
} }
}; };
const onCreateEnvironment = async ({ name, slug }: EnvData) => {
const res = await createEnvironment(workspaceId, {
environmentName: name,
environmentSlug: slug,
});
if (res) {
// TODO: on react-query migration do an api call to resync
setEnvironments((env) => [...env, { name, slug }]);
}
};
const onUpdateEnvironment = async (
oldSlug: string,
{ name, slug }: EnvData
) => {
const res = await updateEnvironment(workspaceId, {
oldEnvironmentSlug: oldSlug,
environmentName: name,
environmentSlug: slug,
});
// TODO: on react-query migration do an api call to resync
if (res) {
setEnvironments((env) =>
env.map((el) => (el.slug === oldSlug ? { name, slug } : el))
);
}
};
const onDeleteEnvironment = async (slugToBeDelete: string) => {
const res = await deleteEnvironment(workspaceId, slugToBeDelete);
// TODO: on react-query migration do an api call to resync
if (res) {
setEnvironments((env) =>
env.filter(({ slug }) => slug !== slugToBeDelete)
);
}
};
return ( return (
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'> <div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
<Head> <Head>
@@ -89,9 +159,12 @@ export default function SettingsBasic() {
</Head> </Head>
<AddServiceTokenDialog <AddServiceTokenDialog
isOpen={isAddServiceTokenDialogOpen} isOpen={isAddServiceTokenDialogOpen}
workspaceId={router.query.id} workspaceId={workspaceId}
environments={environments}
closeModal={closeAddServiceTokenModal} closeModal={closeAddServiceTokenModal}
workspaceName={workspaceName} workspaceName={workspaceName}
serviceTokens={serviceTokens}
setServiceTokens={setServiceTokens}
/> />
<div className='flex flex-row mr-6 max-w-5xl'> <div className='flex flex-row mr-6 max-w-5xl'>
<div className='w-full max-h-screen pb-2 overflow-y-auto'> <div className='w-full max-h-screen pb-2 overflow-y-auto'>
@@ -112,12 +185,13 @@ export default function SettingsBasic() {
<div className='flex flex-col ml-6 text-mineshaft-50'> <div className='flex flex-col ml-6 text-mineshaft-50'>
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='min-w-md mt-2 flex flex-col items-start'> <div className='min-w-md mt-2 flex flex-col items-start'>
<div className='bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start w-full mb-6'> <div className='bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2'>
<p className='text-xl font-semibold mb-4'> <p className='text-xl font-semibold mb-4 mt-2'>
{t('common:display-name')} {t('common:display-name')}
</p> </p>
<div className='max-h-28 w-full max-w-md mr-auto'> <div className='max-h-28 w-full max-w-md mr-auto'>
<InputField <InputField
label=''
onChangeHandler={modifyWorkspaceName} onChangeHandler={modifyWorkspaceName}
type='varName' type='varName'
value={workspaceName} value={workspaceName}
@@ -128,7 +202,7 @@ export default function SettingsBasic() {
<div className='flex justify-start w-full'> <div className='flex justify-start w-full'>
<div className={`flex justify-start max-w-sm mt-4 mb-2`}> <div className={`flex justify-start max-w-sm mt-4 mb-2`}>
<Button <Button
text={t('common:save-changes')} text={t('common:save-changes') as string}
onButtonPressed={() => submitChanges(workspaceName)} onButtonPressed={() => submitChanges(workspaceName)}
color='mineshaft' color='mineshaft'
size='md' size='md'
@@ -139,7 +213,7 @@ export default function SettingsBasic() {
</div> </div>
</div> </div>
</div> </div>
<div className='bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start w-full mb-6 mt-4'> <div className='bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start w-full mb-6 mt-4'>
<p className='text-xl font-semibold self-start'> <p className='text-xl font-semibold self-start'>
{t('common:project-id')} {t('common:project-id')}
</p> </p>
@@ -158,30 +232,70 @@ export default function SettingsBasic() {
{t('settings-project:docs')} {t('settings-project:docs')}
</a> </a>
</p> </p>
<div className='max-h-28 w-ful'> <p className='mt-4 text-xs text-bunker-300'>
<InputField {t('settings-project:auto-generated')}
type='varName' </p>
value={router.query.id} <div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400'>
placeholder='' <p className='mr-2 font-bold pl-4'>{`${t(
isRequired 'common:project-id'
static )}:`}</p>
text={t('settings-project:auto-generated')} <input
/> type='text'
value={workspaceId}
id='myInput'
className='bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none'
disabled
></input>
<div className='group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button
onClick={copyToClipboard}
className='pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
>
{projectIdCopied ? (
<FontAwesomeIcon icon={faCheck} className='pr-0.5' />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm'>
{t('common:click-to-copy')}
</span>
</div>
</div> </div>
</div> </div>
<div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'> <div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'>
<EnvironmentTable
data={environments}
onCreateEnv={onCreateEnvironment}
onUpdateEnv={onUpdateEnvironment}
onDeleteEnv={onDeleteEnvironment}
/>
</div>
<div className='bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-4 mb-4 pt-2'>
<div className='flex flex-row justify-between w-full'> <div className='flex flex-row justify-between w-full'>
<div className='flex flex-col w-full'> <div className='flex flex-col w-full'>
<p className='text-xl font-semibold mb-3'> <p className='text-xl font-semibold mb-3'>
{t('section-token:service-tokens')} {t('section-token:service-tokens')}
</p> </p>
<p className='text-base text-gray-400 mb-4'> <p className='text-sm text-gray-400'>
{t('section-token:service-tokens-description')} {t('section-token:service-tokens-description')}
</p> </p>
<p className='text-sm text-gray-400 mb-4'>
Please, make sure you are on the
<a
className='text-primary underline underline-offset-2 ml-1'
href='https://infisical.com/docs/cli/overview'
target='_blank'
rel='noreferrer'
>
latest version of CLI
</a>
.
</p>
</div> </div>
<div className='w-48'> <div className='w-48 mt-2'>
<Button <Button
text={t('section-token:add-new')} text={t('section-token:add-new') as string}
onButtonPressed={() => { onButtonPressed={() => {
setIsAddServiceTokenDialogOpen(true); setIsAddServiceTokenDialogOpen(true);
}} }}
@@ -194,14 +308,12 @@ export default function SettingsBasic() {
<ServiceTokenTable <ServiceTokenTable
data={serviceTokens} data={serviceTokens}
workspaceName={workspaceName} workspaceName={workspaceName}
setServiceTokens={setServiceTokens as any}
/> />
</div> </div>
<div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'>
<EnvironmentTable data={environments} />
</div>
</div> </div>
</div> </div>
<div className='bg-white/5 rounded-md px-6 pt-6 pb-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4'> <div className='bg-white/5 rounded-md px-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2'>
<p className='text-xl font-bold text-red'> <p className='text-xl font-bold text-red'>
{t('settings-project:danger-zone')} {t('settings-project:danger-zone')}
</p> </p>
@@ -239,7 +351,7 @@ export default function SettingsBasic() {
SettingsBasic.requireAuth = true; SettingsBasic.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps([ export const getServerSideProps = getTranslatedServerSideProps([
"settings", 'settings',
"settings-project", 'settings-project',
"section-token", 'section-token',
]); ]);