From 84700308f5201b70c3680727de0d93eedf7ab32d Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Wed, 11 Jan 2023 23:09:34 +0530 Subject: [PATCH] feat(#31): implemented ui for multi env and integrated api with backend fix(#31): fixed all v2 release conflict --- backend/src/app.ts | 2 +- .../v1/integrationAuthController.ts | 2 +- .../controllers/v2/environmentController.ts | 121 ++- backend/src/ee/models/secretVersion.ts | 5 - backend/src/models/integration.ts | 2 +- backend/src/routes/v2/environment.ts | 25 +- backend/src/routes/v2/secrets.ts | 4 +- frontend/components/basic/Listbox.tsx | 2 +- .../basic/dialog/AddServiceTokenDialog.js | 164 ++-- ...log.tsx => AddUpdateEnvironmentDialog.tsx} | 26 +- .../basic/dialog/DeleteActionModal.tsx | 6 +- .../basic/table/EnvironmentsTable.tsx | 73 +- .../basic/table/ServiceTokenTable.tsx | 3 +- .../components/integrations/Integration.tsx | 10 +- .../utilities/secrets/downloadDotEnv.ts | 3 +- .../utilities/secrets/getSecretsForProject.ts | 6 +- .../api/environments/createEnvironment.ts | 29 + .../api/environments/deleteEnvironment.ts | 26 + .../api/environments/updateEnvironment.ts | 33 + frontend/pages/api/workspace/getWorkspaces.ts | 1 + frontend/pages/dashboard/[id].tsx | 829 +++++++++++------- frontend/pages/settings/project/[id].js | 306 ------- frontend/pages/settings/project/[id].tsx | 236 +++-- 23 files changed, 1057 insertions(+), 857 deletions(-) rename frontend/components/basic/dialog/{AddEnvironmentDialog.tsx => AddUpdateEnvironmentDialog.tsx} (87%) create mode 100644 frontend/pages/api/environments/createEnvironment.ts create mode 100644 frontend/pages/api/environments/deleteEnvironment.ts create mode 100644 frontend/pages/api/environments/updateEnvironment.ts delete mode 100644 frontend/pages/settings/project/[id].js diff --git a/backend/src/app.ts b/backend/src/app.ts index 571b84cd46..82275b9c9e 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -104,12 +104,12 @@ app.use('/api/v1/integration', v1IntegrationRouter); app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); // v2 routes +app.use('/api/v2/workspace', v2EnvironmentRouter); 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/secrets', v2SecretsRouter); app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route app.use('/api/v2/api-key-data', v2APIKeyDataRouter); -app.use('/api/v2/environments', v2EnvironmentRouter); // api docs app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index b2f93a8c29..5df8b4e910 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node'; import axios from 'axios'; import { readFileSync } from 'fs'; 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 { getApps, revokeAccess } from '../../integrations'; diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 77c4fcb591..1d1ffb6a69 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -1,6 +1,13 @@ import { Request, Response } from 'express'; 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 @@ -12,25 +19,24 @@ export const createWorkspaceEnvironment = async ( req: Request, res: Response ) => { - const { workspaceId, environmentName, environmentSlug } = req.body; + const { workspaceId } = req.params; + const { environmentName, environmentSlug } = req.body; try { - // atomic create the environment - const workspace = await Workspace.findOneAndUpdate( - { - _id: workspaceId, - 'environments.slug': { $ne: environmentSlug }, - 'environments.name': { $ne: environmentName }, - }, - { - $addToSet: { - environments: { name: environmentName, slug: environmentSlug }, - }, - } - ); - - if (!workspace) { - throw new Error('Failed to update workspace environment'); + const workspace = await Workspace.findById(workspaceId).exec(); + if ( + !workspace || + workspace?.environments.find( + ({ name, slug }) => slug === environmentSlug || environmentName === name + ) + ) { + throw new Error('Failed to create workspace environment'); } + + workspace?.environments.push({ + name: environmentName.toLowerCase(), + slug: environmentSlug.toLowerCase(), + }); + await workspace.save(); } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); @@ -60,8 +66,8 @@ export const renameWorkspaceEnvironment = async ( req: Request, res: Response ) => { - const { workspaceId, environmentName, environmentSlug, oldEnvironmentSlug } = - req.body; + const { workspaceId } = req.params; + const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; try { // user should pass both new slug and env name if (!environmentSlug || !environmentName) { @@ -69,25 +75,47 @@ export const renameWorkspaceEnvironment = async ( } // atomic update the env to avoid conflict - const workspace = await Workspace.findOneAndUpdate( - { _id: workspaceId, 'environments.slug': oldEnvironmentSlug }, - { - 'environments.$.name': environmentName, - 'environments.$.slug': environmentSlug, - } - ); + const workspace = await Workspace.findById(workspaceId).exec(); 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( { workspace: workspaceId, environment: oldEnvironmentSlug }, { environment: environmentSlug } ); + await SecretVersion.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); await ServiceToken.updateMany( { workspace: workspaceId, environment: oldEnvironmentSlug }, { environment: environmentSlug } ); + await ServiceTokenData.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); await Integration.updateMany( { workspace: workspaceId, environment: oldEnvironmentSlug }, { environment: environmentSlug } @@ -120,37 +148,46 @@ export const deleteWorkspaceEnvironment = async ( req: Request, res: Response ) => { - const { workspaceId, environmentSlug } = req.body; + const { workspaceId } = req.params; + const { environmentSlug } = req.body; try { - // atomic delete the env in the workspacce - const workspace = await Workspace.findOneAndUpdate( - { _id: workspaceId }, - { - $pull: { - environments: { - slug: environmentSlug, - }, - }, - } - ); + // atomic update the env to avoid conflict + const workspace = await Workspace.findById(workspaceId).exec(); 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 await Secret.deleteMany({ workspace: workspaceId, environment: environmentSlug, }); + await SecretVersion.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); await ServiceToken.deleteMany({ workspace: workspaceId, environment: environmentSlug, }); + await ServiceTokenData.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); await Integration.deleteMany({ workspace: workspaceId, environment: environmentSlug, }); - } catch (err) { Sentry.setUser({ email: req.user.email }); Sentry.captureException(err); diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index 616d44fbdc..a4fec0a39d 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose'; import { SECRET_SHARED, SECRET_PERSONAL, - ENV_DEV, - ENV_TESTING, - ENV_STAGING, - ENV_PROD } from '../../variables'; export interface ISecretVersion { @@ -56,7 +52,6 @@ const secretVersionSchema = new Schema( }, environment: { type: String, - enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], required: true }, isDeleted: { // consider removing field diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 0b184ed67a..98e4934d90 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -9,7 +9,7 @@ import { export interface IIntegration { _id: Types.ObjectId; workspace: Types.ObjectId; - environment: 'dev' | 'test' | 'staging' | 'prod'; + environment: string; isActive: boolean; app: string; target: string; diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index 592a512482..924db18fcd 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import express, { Response, Request } from 'express'; const router = express.Router(); import { body, param } from 'express-validator'; import { environmentController } from '../../controllers/v2'; @@ -7,14 +7,15 @@ import { requireWorkspaceAuth, validateRequest, } from '../../middleware'; -import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables'; +import { ADMIN, MEMBER } from '../../variables'; router.post( - '/:workspaceId', - requireAuth, + '/:workspaceId/environments', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED], }), param('workspaceId').exists().trim(), body('environmentSlug').exists().trim(), @@ -24,11 +25,12 @@ router.post( ); router.put( - '/:workspaceId', - requireAuth, + '/:workspaceId/environments', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - acceptedStatuses: [COMPLETED, GRANTED], }), param('workspaceId').exists().trim(), body('environmentSlug').exists().trim(), @@ -39,11 +41,12 @@ router.put( ); router.delete( - '/:workspaceId', - requireAuth, + '/:workspaceId/environments', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - acceptedStatuses: [GRANTED], }), param('workspaceId').exists().trim(), body('environmentSlug').exists().trim(), diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 29eac042fe..4196cd8195 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -18,7 +18,7 @@ import { router.post( '/', body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim().isIn(['dev', 'staging', 'prod', 'test']), + body('environment').exists().isString().trim(), body('secrets') .exists() .custom((value) => { @@ -73,7 +73,7 @@ router.post( router.get( '/', query('workspaceId').exists().trim(), - query('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']), + query('environment').exists().trim(), validateRequest, requireAuth({ acceptedAuthModes: ['jwt', 'serviceToken'] diff --git a/frontend/components/basic/Listbox.tsx b/frontend/components/basic/Listbox.tsx index 95e3f33c64..d315df2e62 100644 --- a/frontend/components/basic/Listbox.tsx +++ b/frontend/components/basic/Listbox.tsx @@ -69,7 +69,7 @@ export default function ListBox({ - `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' : '' } ${ active && !selected diff --git a/frontend/components/basic/dialog/AddServiceTokenDialog.js b/frontend/components/basic/dialog/AddServiceTokenDialog.js index 177911973a..a8c5c15a74 100644 --- a/frontend/components/basic/dialog/AddServiceTokenDialog.js +++ b/frontend/components/basic/dialog/AddServiceTokenDialog.js @@ -8,7 +8,6 @@ import nacl from "tweetnacl"; import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; -import { envMapping } from "../../../public/data/frequentConstants"; import { decryptAssymmetric, encryptAssymmetric, @@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({ workspaceId, workspaceName, serviceTokens, + environments, setServiceTokens }) => { const [serviceToken, setServiceToken] = useState(""); const [serviceTokenName, setServiceTokenName] = useState(""); - const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); + const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]); const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); const [serviceTokenCopied, setServiceTokenCopied] = useState(false); const { t } = useTranslation(); @@ -66,7 +66,7 @@ const AddServiceTokenDialog = ({ let newServiceToken = await addServiceToken({ name: serviceTokenName, workspaceId, - environment: envMapping[serviceTokenEnv], + environment: selectedServiceTokenEnv.slug, expiresIn: expiryMapping[serviceTokenExpiresIn], encryptedKey: ciphertext, iv, @@ -101,155 +101,159 @@ const AddServiceTokenDialog = ({ }; return ( -
+
- + -
+
-
-
+
+
- {serviceToken == "" ? ( - + {serviceToken == '' ? ( + - {t("section-token:add-dialog.title", { + {t('section-token:add-dialog.title', { target: workspaceName, })} -
-
-

- {t("section-token:add-dialog.description")} +

+
+

+ {t('section-token:add-dialog.description')}

-
+
-
+
name)} + onChange={(envName) => + setSelectedServiceTokenEnv( + environments.find( + ({ name }) => envName === name + ) || { + name: 'unknown', + slug: 'unknown', + } + ) + } isFull={true} - text={`${t("common:environment")}: `} + text={`${t('common:environment')}: `} />
-
+
-
-
+
+
) : ( - + - {t("section-token:add-dialog.copy-service-token")} + {t('section-token:add-dialog.copy-service-token')} -
-
-

+

+
+

{t( - "section-token:add-dialog.copy-service-token-description" + 'section-token:add-dialog.copy-service-token-description' )}

-
-
+
+
-
+
{serviceToken}
-
+
- - {t("common:click-to-copy")} + + {t('common:click-to-copy')}
-
+
diff --git a/frontend/components/basic/dialog/AddEnvironmentDialog.tsx b/frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx similarity index 87% rename from frontend/components/basic/dialog/AddEnvironmentDialog.tsx rename to frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx index 83b578d81b..9476dd8f65 100644 --- a/frontend/components/basic/dialog/AddEnvironmentDialog.tsx +++ b/frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx @@ -12,15 +12,24 @@ type Props = { // on edit mode load up initial values initialValues?: FormFields; 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 * @param {*} param0 * @returns */ -export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues, isEditMode }: Props) => { +export const AddUpdateEnvironmentDialog = ({ + isOpen, + onClose, + onCreateSubmit, + onEditSubmit, + initialValues, + isEditMode, +}: Props) => { const [formInput, setFormInput] = useState({ name: '', slug: '', @@ -39,7 +48,15 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues, const onFormSubmit: FormEventHandler = (e) => { e.preventDefault(); - console.log(formInput); + const data = { + name: formInput.name.toLowerCase(), + slug: formInput.slug.toLowerCase(), + }; + if (isEditMode) { + onEditSubmit(data); + return; + } + onCreateSubmit(data); }; return ( @@ -81,7 +98,7 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues,
onInputChange('name', val)} type='varName' value={formInput.name} @@ -112,6 +129,7 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues, type='submit' color='mineshaft' text={isEditMode ? 'Update' : 'Create'} + active={formInput.name !== '' && formInput.slug !== ''} size='md' />
diff --git a/frontend/components/basic/dialog/DeleteActionModal.tsx b/frontend/components/basic/dialog/DeleteActionModal.tsx index cafd832aeb..9a1b7a46af 100644 --- a/frontend/components/basic/dialog/DeleteActionModal.tsx +++ b/frontend/components/basic/dialog/DeleteActionModal.tsx @@ -1,4 +1,4 @@ -import { Fragment, useState } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { Dialog, Transition } from '@headlessui/react'; import InputField from '../InputField'; @@ -21,6 +21,10 @@ const DeleteActionModal = ({ }:Props) => { const [deleteInputField, setDeleteInputField] = useState("") + useEffect(() => { + setDeleteInputField(""); + }, [isOpen]); + return (
diff --git a/frontend/components/basic/table/EnvironmentsTable.tsx b/frontend/components/basic/table/EnvironmentsTable.tsx index 501b7f9bc2..56536b679e 100644 --- a/frontend/components/basic/table/EnvironmentsTable.tsx +++ b/frontend/components/basic/table/EnvironmentsTable.tsx @@ -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 {AddEnvironmentDialog} from '../dialog/AddEnvironmentDialog'; +import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog'; import DeleteActionModal from '../dialog/DeleteActionModal'; -const EnvironmentTable = ({ data = [] }) => { - const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - 'createUpdateEnv', - 'deleteEnv', - ] as const); +type Env = { name: string; slug: string }; + +type Props = { + data: Env[]; + onCreateEnv: (arg0: Env) => Promise; + onUpdateEnv: (oldSlug: string, arg0: Env) => Promise; + onDeleteEnv: (slug: string) => Promise; +}; + +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)?.slug, + env + ); + handlePopUpClose('createUpdateEnv'); + } catch (error) { + console.error(error); + } + }; + + const onEnvDeleteCB = async () => { + try { + await onDeleteEnv( + (popUp.deleteEnv?.data as Pick)?.slug + ); + handlePopUpClose('deleteEnv'); + } catch (error) { + console.error(error); + } + }; return ( <> @@ -62,7 +108,9 @@ const EnvironmentTable = ({ data = [] }) => {
diff --git a/frontend/components/basic/table/ServiceTokenTable.tsx b/frontend/components/basic/table/ServiceTokenTable.tsx index 412a0dbfbc..4d30b30139 100644 --- a/frontend/components/basic/table/ServiceTokenTable.tsx +++ b/frontend/components/basic/table/ServiceTokenTable.tsx @@ -3,7 +3,6 @@ import { faX } from '@fortawesome/free-solid-svg-icons'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; -import { reverseEnvMapping } from '../../../public/data/frequentConstants'; import guidGenerator from '../../utilities/randomId'; import Button from '../buttons/Button'; @@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok {workspaceName} - {reverseEnvMapping[row.environment]} + {row.environment} {new Date(row.expiresAt).toUTCString()} diff --git a/frontend/components/integrations/Integration.tsx b/frontend/components/integrations/Integration.tsx index 11edf7cb70..73d6ee0dc8 100644 --- a/frontend/components/integrations/Integration.tsx +++ b/frontend/components/integrations/Integration.tsx @@ -15,9 +15,7 @@ import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps" import updateIntegration from "../../pages/api/integrations/updateIntegration" import { contextNetlifyMapping, - envMapping, reverseContextNetlifyMapping, - reverseEnvMapping, } from "../../public/data/frequentConstants"; interface Integration { @@ -41,9 +39,7 @@ const Integration = ({ }: { integration: Integration; }) => { - const [integrationEnvironment, setIntegrationEnvironment] = useState( - reverseEnvMapping[integration.environment] - ); + const [integrationEnvironment, setIntegrationEnvironment] = useState(integration.environment); const [fileState, setFileState] = useState([]); const router = useRouter(); const [apps, setApps] = useState([]); // integration app objects @@ -199,9 +195,9 @@ const Integration = ({ const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined const siteId = siteApp?.siteId ? siteApp.siteId : null; - const result = await updateIntegration({ + await updateIntegration({ integrationId: integration._id, - environment: envMapping[integrationEnvironment], + environment: integrationEnvironment, app: integrationApp, isActive: true, target: integrationTarget ? integrationTarget.toLowerCase() : null, diff --git a/frontend/components/utilities/secrets/downloadDotEnv.ts b/frontend/components/utilities/secrets/downloadDotEnv.ts index dbb29497b1..4a84aa17aa 100644 --- a/frontend/components/utilities/secrets/downloadDotEnv.ts +++ b/frontend/components/utilities/secrets/downloadDotEnv.ts @@ -1,4 +1,3 @@ -import { envMapping } from "../../../public/data/frequentConstants"; import checkOverrides from './checkOverrides'; @@ -39,7 +38,7 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str const fileDownloadUrl = URL.createObjectURL(blob); const alink = document.createElement('a'); alink.href = fileDownloadUrl; - alink.download = envMapping[env] + '.env'; + alink.download = env + '.env'; alink.click(); } diff --git a/frontend/components/utilities/secrets/getSecretsForProject.ts b/frontend/components/utilities/secrets/getSecretsForProject.ts index 81de27be09..629ac98ee3 100644 --- a/frontend/components/utilities/secrets/getSecretsForProject.ts +++ b/frontend/components/utilities/secrets/getSecretsForProject.ts @@ -1,8 +1,6 @@ import getSecrets from '~/pages/api/files/GetSecrets'; import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; -import { envMapping } from '../../../public/data/frequentConstants'; - const { decryptAssymmetric, decryptSymmetric @@ -35,7 +33,7 @@ interface SecretProps { } interface FunctionProps { - env: keyof typeof envMapping; + env: string; setIsKeyAvailable: any; setData: any; workspaceId: string; @@ -58,7 +56,7 @@ const getSecretsForProject = async ({ try { let encryptedSecrets; try { - encryptedSecrets = await getSecrets(workspaceId, envMapping[env]); + encryptedSecrets = await getSecrets(workspaceId, env); } catch (error) { console.log('ERROR: Not able to access the latest version of secrets'); } diff --git a/frontend/pages/api/environments/createEnvironment.ts b/frontend/pages/api/environments/createEnvironment.ts new file mode 100644 index 0000000000..2d0ecfa379 --- /dev/null +++ b/frontend/pages/api/environments/createEnvironment.ts @@ -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; diff --git a/frontend/pages/api/environments/deleteEnvironment.ts b/frontend/pages/api/environments/deleteEnvironment.ts new file mode 100644 index 0000000000..de86115321 --- /dev/null +++ b/frontend/pages/api/environments/deleteEnvironment.ts @@ -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; diff --git a/frontend/pages/api/environments/updateEnvironment.ts b/frontend/pages/api/environments/updateEnvironment.ts new file mode 100644 index 0000000000..65fb449a8b --- /dev/null +++ b/frontend/pages/api/environments/updateEnvironment.ts @@ -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; diff --git a/frontend/pages/api/workspace/getWorkspaces.ts b/frontend/pages/api/workspace/getWorkspaces.ts index 1bbb42c7a7..d77c4c431b 100644 --- a/frontend/pages/api/workspace/getWorkspaces.ts +++ b/frontend/pages/api/workspace/getWorkspaces.ts @@ -5,6 +5,7 @@ interface Workspace { _id: string; name: string; organization: string; + environments: Array<{name:string, slug:string}> } /** diff --git a/frontend/pages/dashboard/[id].tsx b/frontend/pages/dashboard/[id].tsx index 4ce91002d3..e20ee73c72 100644 --- a/frontend/pages/dashboard/[id].tsx +++ b/frontend/pages/dashboard/[id].tsx @@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react'; import Head from 'next/head'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useTranslation } from "next-i18next"; +import { useTranslation } from 'next-i18next'; import { faArrowDownAZ, faArrowDownZA, @@ -34,7 +34,6 @@ import getSecretsForProject from '~/components/utilities/secrets/getSecretsForPr import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps'; import guidGenerator from '~/utilities/randomId'; -import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants'; import addSecrets from '../api/files/AddSecrets'; import deleteSecrets from '../api/files/DeleteSecrets'; import updateSecrets from '../api/files/UpdateSecrets'; @@ -43,6 +42,10 @@ import checkUserAction from '../api/userActions/checkUserAction'; import registerUserAction from '../api/userActions/registerUserAction'; import getWorkspaces from '../api/workspace/getWorkspaces'; +type WorkspaceEnv = { + name: string; + slug: string; +}; interface SecretDataProps { type: 'personal' | 'shared'; @@ -68,7 +71,7 @@ interface SnapshotProps { secretVersions: { id: string; pos: number; - type: "personal" | "shared"; + type: 'personal' | 'shared'; environment: string; key: string; value: string; @@ -99,14 +102,11 @@ function findDuplicates(arr: any[]) { */ export default function Dashboard() { const [data, setData] = useState(); - const [initialData, setInitialData] = useState([]); + const [initialData, setInitialData] = useState([]); const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); - const [workspaceId, setWorkspaceId] = useState(''); const [blurred, setBlurred] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true); - const [env, setEnv] = useState('Development'); - const [snapshotEnv, setSnapshotEnv] = useState('Development'); const [isNew, setIsNew] = useState(false); const [isLoading, setIsLoading] = useState(false); const [searchKeys, setSearchKeys] = useState(''); @@ -114,7 +114,7 @@ export default function Dashboard() { const [sortMethod, setSortMethod] = useState('alphabetical'); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false); - const [sidebarSecretId, toggleSidebar] = useState("None"); + const [sidebarSecretId, toggleSidebar] = useState('None'); const [PITSidebarOpen, togglePITSidebar] = useState(false); const [sharedToHide, setSharedToHide] = useState([]); const [snapshotData, setSnapshotData] = useState(); @@ -123,6 +123,16 @@ export default function Dashboard() { const { t } = useTranslation(); const { createNotification } = useNotificationContext(); + const workspaceId = router.query.id as string; + const [workspaceEnvs, setWorkspaceEnvs] = useState([]); + + const [selectedSnapshotEnv, setSelectedSnapshotEnv] = + useState(); + const [selectedEnv, setSelectedEnv] = useState({ + name: '', + slug: '', + }); + // #TODO: fix save message for changing reroutes // const beforeRouteHandler = (url) => { // const warningText = @@ -169,25 +179,37 @@ export default function Dashboard() { useEffect(() => { (async () => { try { - const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) }) + const tempNumSnapshots = await getProjectSercetSnapshotsCount({ + workspaceId, + }); setNumSnapshots(tempNumSnapshots); const userWorkspaces = await getWorkspaces(); - const listWorkspaces = userWorkspaces.map((workspace) => workspace._id); - if ( - !listWorkspaces.includes(router.asPath.split('/')[2]) - ) { - router.push('/dashboard/' + listWorkspaces[0]); + const workspace = userWorkspaces.find( + (workspace) => workspace._id === workspaceId + ); + if (!workspace) { + router.push('/dashboard/' + userWorkspaces?.[0]?._id); } + setWorkspaceEnvs(workspace?.environments || []); + // set env + const env = workspace?.environments?.[0] || { + name: 'unknown', + slug: 'unkown', + }; + setSelectedEnv(env); + setSelectedSnapshotEnv(env); const user = await getUser(); setIsNew( - (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3 + (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / + 60000 < + 3 ? true : false ); const userAction = await checkUserAction({ - action: 'first_time_secrets_pushed' + action: 'first_time_secrets_pushed', }); setHasUserEverPushed(userAction ? true : false); } catch (error) { @@ -202,13 +224,12 @@ export default function Dashboard() { try { setIsLoading(true); setBlurred(true); - setWorkspaceId(String(router.query.id)); - + // ENV const dataToSort = await getSecretsForProject({ - env, + env: selectedEnv.slug, setIsKeyAvailable, setData, - workspaceId: String(router.query.id) + workspaceId, }); setInitialData(dataToSort); reorderRows(dataToSort); @@ -230,7 +251,7 @@ export default function Dashboard() { } })(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [env]); + }, [selectedEnv]); const addRow = () => { setIsNew(false); @@ -243,17 +264,17 @@ export default function Dashboard() { value: '', type: 'shared', comment: '', - } + }, ]); }; /** * This function add an ovverrided version of a certain secret to the current user - * @param {object} obj + * @param {object} obj * @param {string} obj.id - if of this secret that is about to be overriden * @param {string} obj.keyName - key name of this secret * @param {string} obj.value - value of this secret - * @param {string} obj.pos - position of this secret on the dashboard + * @param {string} obj.pos - position of this secret on the dashboard */ const addOverride = ({ id, keyName, value, pos, comment }: overrideProps) => { setIsNew(false); @@ -265,20 +286,32 @@ export default function Dashboard() { key: keyName, value: value, type: 'personal', - comment: comment - } + comment: comment, + }, ]; - sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); + sortValuesHandler( + tempdata, + sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical' + ); }; - const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { + const deleteRow = ({ + ids, + secretName, + }: { + ids: string[]; + secretName: string; + }) => { setButtonReady(true); - toggleSidebar("None"); + toggleSidebar('None'); createNotification({ text: `${secretName} has been deleted. Remember to save changes.`, - type: 'error' + type: 'error', }); - sortValuesHandler(data!.filter((row: SecretDataProps) => !ids.includes(row.id)), sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical"); + sortValuesHandler( + data!.filter((row: SecretDataProps) => !ids.includes(row.id)), + sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical' + ); }; /** @@ -289,15 +322,30 @@ export default function Dashboard() { setButtonReady(true); // find which shared secret corresponds to the overriden version - const sharedVersionOfOverride = data!.filter(secret => secret.type == "shared" && secret.key == data!.filter(row => row.id == id)[0]?.key)[0]?.id; - + const sharedVersionOfOverride = data!.filter( + (secret) => + secret.type == 'shared' && + secret.key == data!.filter((row) => row.id == id)[0]?.key + )[0]?.id; + // change the sidebar to this shared secret; and unhide it - toggleSidebar(sharedVersionOfOverride) - setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride)) + toggleSidebar(sharedVersionOfOverride); + setSharedToHide( + sharedToHide!.filter((tempId) => tempId != sharedVersionOfOverride) + ); // resort secrets - const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal')) - sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical") + const tempData = data!.filter( + (row: SecretDataProps) => + !( + row.key == data!.filter((row) => row.id == id)[0]?.key && + row.type == 'personal' + ) + ); + sortValuesHandler( + tempData, + sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical' + ); }; const modifyValue = (value: string, pos: number) => { @@ -351,59 +399,97 @@ export default function Dashboard() { const obj = Object.assign( {}, - ...newData!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] })) + ...newData!.map((row: SecretDataProps) => ({ + [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''], + })) ); // Checking if any of the secret keys start with a number - if so, don't do anything const nameErrors = !Object.keys(obj) .map((key) => !isNaN(Number(key[0].charAt(0)))) .every((v) => v === false); - const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0; + const duplicatesExist = + findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)) + .length > 0; if (nameErrors) { return createNotification({ text: 'Solve all name errors before saving secrets.', - type: 'error' + type: 'error', }); } if (duplicatesExist) { return createNotification({ text: 'Remove duplicated secret names before saving.', - type: 'error' + type: 'error', }); } // Once "Save changes" is clicked, disable that button setButtonReady(false); - const secretsToBeDeleted - = initialData - .filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)) - .map(secret => secret.id); - - const secretsToBeAdded - = newData! - .filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id)); - - const secretsToBeUpdated - = newData!.filter(newDataPoint => initialData - .filter(initDataPoint => newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id) - && (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value - || newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key - || newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment)) - .map(secret => secret.id).includes(newDataPoint.id)); - + const secretsToBeDeleted = initialData + .filter( + (initDataPoint) => + !newData! + .map((newDataPoint) => newDataPoint.id) + .includes(initDataPoint.id) + ) + .map((secret) => secret.id); + + const secretsToBeAdded = newData!.filter( + (newDataPoint) => + !initialData + .map((initDataPoint) => initDataPoint.id) + .includes(newDataPoint.id) + ); + + const secretsToBeUpdated = newData!.filter((newDataPoint) => + initialData + .filter( + (initDataPoint) => + newData! + .map((newDataPoint) => newDataPoint.id) + .includes(initDataPoint.id) && + (newData!.filter( + (newDataPoint) => newDataPoint.id == initDataPoint.id + )[0].value != initDataPoint.value || + newData!.filter( + (newDataPoint) => newDataPoint.id == initDataPoint.id + )[0].key != initDataPoint.key || + newData!.filter( + (newDataPoint) => newDataPoint.id == initDataPoint.id + )[0].comment != initDataPoint.comment) + ) + .map((secret) => secret.id) + .includes(newDataPoint.id) + ); + if (secretsToBeDeleted.length > 0) { await deleteSecrets({ secretIds: secretsToBeDeleted }); } + // ENV if (secretsToBeAdded.length > 0) { - const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] }) - secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId }); + const secrets = await encryptSecrets({ + secretsToEncrypt: secretsToBeAdded, + workspaceId, + env: selectedEnv.slug, + }); + secrets && + (await addSecrets({ + secrets, + env: selectedEnv.slug, + workspaceId, + })); } if (secretsToBeUpdated.length > 0) { - const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] }) - secrets && await updateSecrets({ secrets }); + const secrets = await encryptSecrets({ + secretsToEncrypt: secretsToBeUpdated, + workspaceId, + env: selectedEnv.slug, + }); + secrets && (await updateSecrets({ secrets })); } // If this user has never saved environment variables before, show them a prompt to read docs @@ -425,299 +511,417 @@ export default function Dashboard() { setBlurred(!blurred); }; - const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => { - const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod; + const sortValuesHandler = ( + dataToSort: SecretDataProps[] | 1, + specificSortMethod?: 'alphabetical' | '-alphabetical' + ) => { + const howToSort = + specificSortMethod == undefined ? sortMethod : specificSortMethod; const sortedData = (dataToSort != 1 ? dataToSort : data)! - .sort((a, b) => - howToSort == 'alphabetical' - ? a.key.localeCompare(b.key) - : b.key.localeCompare(a.key) - ) - .map((item: SecretDataProps, index: number) => { - return { - ...item, - pos: index - }; - }); + .sort((a, b) => + howToSort == 'alphabetical' + ? a.key.localeCompare(b.key) + : b.key.localeCompare(a.key) + ) + .map((item: SecretDataProps, index: number) => { + return { + ...item, + pos: index, + }; + }); setData(sortedData); }; - - const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { - deleteRow({ids, secretName}); + + const deleteCertainRow = ({ + ids, + secretName, + }: { + ids: string[]; + secretName: string; + }) => { + deleteRow({ ids, secretName }); }; return data ? ( -
+
- {t("common:head-title", { title: t("dashboard:title") })} - - - - + {t('common:head-title', { title: t('dashboard:title') })} + + + + -
- {sidebarSecretId != "None" && row.key == data.filter(row => row.id == sidebarSecretId)[0]?.key)} - modifyKey={listenChangeKey} - modifyValue={listenChangeValue} - modifyComment={listenChangeComment} - addOverride={addOverride} - deleteOverride={deleteOverride} - buttonReady={buttonReady} - savePush={savePush} - sharedToHide={sharedToHide} - setSharedToHide={setSharedToHide} - deleteRow={deleteCertainRow} - />} - {PITSidebarOpen && } -
- +
+ {sidebarSecretId != 'None' && ( + + row.key == + data.filter((row) => row.id == sidebarSecretId)[0]?.key + )} + modifyKey={listenChangeKey} + modifyValue={listenChangeValue} + modifyComment={listenChangeComment} + addOverride={addOverride} + deleteOverride={deleteOverride} + buttonReady={buttonReady} + savePush={savePush} + sharedToHide={sharedToHide} + setSharedToHide={setSharedToHide} + deleteRow={deleteCertainRow} + /> + )} + {PITSidebarOpen && ( + + )} +
+ {checkDocsPopUpVisible && ( )} -
- {snapshotData && -
-
} -
-
-

{snapshotData ? "Secret Snapshot" : t("dashboard:title")}

- {snapshotData && {new Date(snapshotData.createdAt).toLocaleString()}} +
+ {snapshotData && ( +
+
+ )} +
+
+

{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}

+ {snapshotData && ( + + {new Date(snapshotData.createdAt).toLocaleString()} + + )}
{!snapshotData && data?.length == 0 && ( name)} + onChange={(envName) => + setSelectedEnv( + workspaceEnvs.find(({ name }) => envName === name) || { + name: 'unknown', + slug: 'unknown', + } + ) + } /> )}
-
+
{(data?.length !== 0 || buttonReady) && !snapshotData && (
+ )} + {snapshotData && ( +
+
)} - {snapshotData &&
-
}
-
-
-
+
+
+
{(snapshotData || data?.length !== 0) && ( <> - {!snapshotData - ? - : } -
+ {!snapshotData ? ( + name)} + onChange={(envName) => + setSelectedEnv( + workspaceEnvs.find( + ({ name }) => envName === name + ) || { + name: 'unknown', + slug: 'unknown', + } + ) + } + /> + ) : ( + name)} + onChange={(envName) => + setSelectedSnapshotEnv( + workspaceEnvs.find( + ({ name }) => envName === name + ) || { + name: 'unknown', + slug: 'unknown', + } + ) + } + /> + )} +
setSearchKeys(e.target.value)} - placeholder={String(t("dashboard:search-keys"))} + placeholder={String(t('dashboard:search-keys'))} />
- {!snapshotData &&
-
} - {!snapshotData &&
- -
} -
+ {!snapshotData && ( +
+
+ )} + {!snapshotData && ( +
+ +
+ )} +
- {!snapshotData &&
-
} + {!snapshotData && ( +
+
+ )} )}
{isLoading ? ( -
- infisical loading indicator -
- ) : ( - data?.length !== 0 ? ( -
+
+ infisical loading indicator +
+ ) : data?.length !== 0 ? ( +
-
- {!snapshotData && data?.filter(row => row.key?.toUpperCase().includes(searchKeys.toUpperCase())) - .filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => ( - item.key + item.type) - )?.includes(keyPair.key + keyPair.type)} - toggleSidebar={toggleSidebar} - sidebarSecretId={sidebarSecretId} - isSnapshot={false} - /> - ))} - {snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key)) - .filter(row => reverseEnvMapping[row.environment] == snapshotEnv) - .filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase())) - .filter(row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions - ?.map((item) => item.key) - .filter( - (item, index) => - index !== - snapshotData.secretVersions?.map((item) => item.key).indexOf(item) - ).includes(row.key) && row.type == 'shared'))?.map((item) => item.id).includes(row.id) && row.type == 'shared')).map((keyPair) => ( - item.key + item.type) - )?.includes(keyPair.key + keyPair.type)} - toggleSidebar={toggleSidebar} - sidebarSecretId={sidebarSecretId} - isSnapshot={true} - /> - ))} +
+ {!snapshotData && + data + ?.filter((row) => + row.key + ?.toUpperCase() + .includes(searchKeys.toUpperCase()) + ) + .filter( + (row) => + !( + sharedToHide.includes(row.id) && + row.type == 'shared' + ) + ) + .map((keyPair) => ( + item.key + item.type) + )?.includes(keyPair.key + keyPair.type)} + toggleSidebar={toggleSidebar} + sidebarSecretId={sidebarSecretId} + isSnapshot={false} + /> + ))} + {snapshotData && + snapshotData.secretVersions + ?.sort((a, b) => a.key.localeCompare(b.key)) + .filter( + (row) => + row.environment == selectedSnapshotEnv?.slug + ) + .filter((row) => + row.key + .toUpperCase() + .includes(searchKeys.toUpperCase()) + ) + .filter( + (row) => + !( + snapshotData.secretVersions + ?.filter( + (row) => + snapshotData.secretVersions + ?.map((item) => item.key) + .filter( + (item, index) => + index !== + snapshotData.secretVersions + ?.map((item) => item.key) + .indexOf(item) + ) + .includes(row.key) && row.type == 'shared' + ) + ?.map((item) => item.id) + .includes(row.id) && row.type == 'shared' + ) + ) + .map((keyPair) => ( + item.key + item.type) + )?.includes(keyPair.key + keyPair.type)} + toggleSidebar={toggleSidebar} + sidebarSecretId={sidebarSecretId} + isSnapshot={true} + /> + ))}
- {!snapshotData &&
- -
} + {!snapshotData && ( +
+ +
+ )}
) : ( -
+
{isKeyAvailable && !snapshotData && ( )} - { - (!isKeyAvailable && ( - <> - -

- To view this file, contact your administrator for - permission. -

-

- They need to grant you access in the team tab. -

- - ))} + {!isKeyAvailable && ( + <> + +

+ To view this file, contact your administrator for + permission. +

+

+ They need to grant you access in the team tab. +

+ + )}
- ))} + )}
) : ( -
-
+
+
loading animation
); @@ -766,4 +969,4 @@ export default function Dashboard() { Dashboard.requireAuth = true; -export const getServerSideProps = getTranslatedServerSideProps(["dashboard"]); +export const getServerSideProps = getTranslatedServerSideProps(['dashboard']); diff --git a/frontend/pages/settings/project/[id].js b/frontend/pages/settings/project/[id].js deleted file mode 100644 index 3449df346a..0000000000 --- a/frontend/pages/settings/project/[id].js +++ /dev/null @@ -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 ( -
- - - {t("common:head-title", { title: t("settings-project:title") })} - - - - -
-
- -
-
-

- {t("settings-project:title")} -

-

- {t("settings-project:description")} -

-
-
-
-
-
-
-

- {t("common:display-name")} -

-
- -
-
-
-
-
-
-
-

- {t("common:project-id")} -

-

- {t("settings-project:project-id-description")} -

-

- {t("settings-project:project-id-description2")} - {/* eslint-disable-next-line react/jsx-no-target-blank */} - - {t("settings-project:docs")} - -

-

{t("settings-project:auto-generated")}

-
-

{`${t( - "common:project-id" - )}:`}

- -
- - - {t("common:click-to-copy")} - -
-
-
-
-
-
-

- {t("section-token:service-tokens")} -

-

- {t("section-token:service-tokens-description")} -

-

- Please, make sure you are on the - - latest version of CLI - . -

-
-
-
-
- -
-
-
-
-

- {t("settings-project:danger-zone")} -

-

- {t("settings-project:danger-zone-note")} -

-
- -
- -

- {t("settings-project:delete-project-note")} -

-
-
-
-
-
- ); -} - -SettingsBasic.requireAuth = true; - -export const getServerSideProps = getTranslatedServerSideProps([ - "settings", - "settings-project", - "section-token", -]); diff --git a/frontend/pages/settings/project/[id].tsx b/frontend/pages/settings/project/[id].tsx index 4a64c3e655..619ef2e6be 100644 --- a/frontend/pages/settings/project/[id].tsx +++ b/frontend/pages/settings/project/[id].tsx @@ -1,55 +1,87 @@ -import { useEffect, useRef, useState } from "react"; -import Head from "next/head"; -import { useRouter } from "next/router"; -import { useTranslation } from "next-i18next"; -import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons"; - -import Button from "~/components/basic/buttons/Button"; -import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog"; -import InputField from "~/components/basic/InputField"; +import { useEffect, 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 EnvironmentTable from '~/components/basic/table/EnvironmentsTable'; -import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable"; -import NavHeader from "~/components/navigation/NavHeader"; -import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; +import ServiceTokenTable from '~/components/basic/table/ServiceTokenTable'; +import NavHeader from '~/components/navigation/NavHeader'; +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 deleteWorkspace from "../../api/workspace/deleteWorkspace"; -import getWorkspaces from "../../api/workspace/getWorkspaces"; -import renameWorkspace from "../../api/workspace/renameWorkspace"; +import createEnvironment from '../../api/environments/createEnvironment'; +import getServiceTokens from '../../api/serviceToken/getServiceTokens'; +import deleteWorkspace from '../../api/workspace/deleteWorkspace'; +import getWorkspaces from '../../api/workspace/getWorkspaces'; +import renameWorkspace from '../../api/workspace/renameWorkspace'; +type EnvData = { + name: string; + slug: string; +}; export default function SettingsBasic() { const [buttonReady, setButtonReady] = useState(false); const router = useRouter(); - const [workspaceName, setWorkspaceName] = useState(""); + const [workspaceName, setWorkspaceName] = useState(''); const [serviceTokens, setServiceTokens] = useState([]); - const [environments,setEnvironments] = useState([]); - const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(""); + const [environments, setEnvironments] = useState>([]); + const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState(''); 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(); - useEffect(async () => { - const userWorkspaces = await getWorkspaces(); - userWorkspaces.forEach((userWorkspace) => { - if (userWorkspace._id == router.query.id) { - setWorkspaceName(userWorkspace.name); - setEnvironments(userWorkspace.environments); - } - }); - const tempServiceTokens = await getServiceTokens({ - workspaceId: router.query.id, - }); - setServiceTokens(tempServiceTokens); + /** + * This function copies the project id to the clipboard + */ + function copyToClipboard() { + const copyText = document.getElementById('myInput') as HTMLInputElement; + + if (copyText) { + copyText.select(); + copyText.setSelectionRange(0, 99999); // For mobile devices + + navigator.clipboard.writeText(copyText.value); + + 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); setWorkspaceName(newName); }; - const submitChanges = (newWorkspaceName) => { - renameWorkspace(router.query.id, newWorkspaceName); + const submitChanges = (newWorkspaceName: string) => { + renameWorkspace(workspaceId, newWorkspaceName); setButtonReady(false); }; @@ -69,16 +101,54 @@ export default function SettingsBasic() { if (userWorkspaces.length > 1) { if ( userWorkspaces.filter( - (workspace) => workspace._id == router.query.id + (workspace) => workspace._id === workspaceId )[0].name == workspaceToBeDeletedName ) { - await deleteWorkspace(router.query.id); + await deleteWorkspace(workspaceId); 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 (
@@ -89,9 +159,12 @@ export default function SettingsBasic() {
@@ -112,12 +185,13 @@ export default function SettingsBasic() {
-
-

+

+

{t('common:display-name')}

-
+

{t('common:project-id')}

@@ -158,30 +232,70 @@ export default function SettingsBasic() { {t('settings-project:docs')}

-
- +

+ {t('settings-project:auto-generated')} +

+
+

{`${t( + 'common:project-id' + )}:`}

+ +
+ + + {t('common:click-to-copy')} + +
+ +
+

{t('section-token:service-tokens')}

-

+

{t('section-token:service-tokens-description')}

+

+ Please, make sure you are on the + + latest version of CLI + + . +

-
+
-
- -
-
+

{t('settings-project:danger-zone')}

@@ -239,7 +351,7 @@ export default function SettingsBasic() { SettingsBasic.requireAuth = true; export const getServerSideProps = getTranslatedServerSideProps([ - "settings", - "settings-project", - "section-token", + 'settings', + 'settings-project', + 'section-token', ]);