Merge pull request #186 from akhilmhdh/feat/#31

feat(#31): implemented api for environment crud operations
This commit is contained in:
mv-turtle
2023-01-13 16:45:35 -08:00
committed by GitHub
40 changed files with 1763 additions and 728 deletions
+2
View File
@@ -46,6 +46,7 @@ import {
workspace as v2WorkspaceRouter, workspace as v2WorkspaceRouter,
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
apiKeyData as v2APIKeyDataRouter, apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
} from './routes/v2'; } from './routes/v2';
import { healthCheck } from './routes/status'; import { healthCheck } from './routes/status';
@@ -103,6 +104,7 @@ 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);
@@ -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';
@@ -31,11 +31,17 @@ export const oAuthExchange = async (
if (!INTEGRATION_SET.has(integration)) if (!INTEGRATION_SET.has(integration))
throw new Error('Failed to validate integration'); throw new Error('Failed to validate integration');
const environments = req.membership.workspace?.environments || [];
if(environments.length === 0){
throw new Error("Failed to get environments")
}
await IntegrationService.handleOAuthExchange({ await IntegrationService.handleOAuthExchange({
workspaceId, workspaceId,
integration, integration,
code code,
environment: environments[0].slug,
}); });
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -9,7 +9,6 @@ import {
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { ENV_SET } from '../../variables';
import { postHogClient } from '../../services'; import { postHogClient } from '../../services';
interface PushSecret { interface PushSecret {
@@ -44,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// validate environment // validate environment
if (!ENV_SET.has(environment)) { const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
@@ -116,7 +116,8 @@ export const pullSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// validate environment // validate environment
if (!ENV_SET.has(environment)) { const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
@@ -183,7 +184,8 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// validate environment // validate environment
if (!ENV_SET.has(environment)) { const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
@@ -1,7 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ServiceToken } from '../../models'; import { ServiceToken } from '../../models';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { ENV_SET } from '../../variables';
import { JWT_SERVICE_SECRET } from '../../config'; import { JWT_SERVICE_SECRET } from '../../config';
/** /**
@@ -36,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
} = req.body; } = req.body;
// validate environment // validate environment
if (!ENV_SET.has(environment)) { const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
@@ -0,0 +1,204 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Secret,
ServiceToken,
Workspace,
Integration,
ServiceTokenData,
} from '../../models';
import { SecretVersion } from '../../ee/models';
/**
* Create new workspace environment named [environmentName] under workspace with id
* @param req
* @param res
* @returns
*/
export const createWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body;
try {
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);
return res.status(400).send({
message: 'Failed to create new workspace environment',
});
}
return res.status(200).send({
message: 'Successfully created new environment',
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
});
};
/**
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
* Old slug [oldEnvironmentSlug] must be provided
* @param req
* @param res
* @returns
*/
export const renameWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try {
// user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!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 }
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace environment',
});
}
return res.status(200).send({
message: 'Successfully update environment',
workspace: workspaceId,
environment: {
name: environmentName,
slug: environmentSlug,
},
});
};
/**
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
* @param req
* @param res
* @returns
*/
export const deleteWorkspaceEnvironment = async (
req: Request,
res: Response
) => {
const { workspaceId } = req.params;
const { environmentSlug } = req.body;
try {
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
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);
return res.status(400).send({
message: 'Failed to delete workspace environment',
});
}
return res.status(200).send({
message: 'Successfully deleted environment',
workspace: workspaceId,
environment: environmentSlug,
});
};
+3 -1
View File
@@ -3,11 +3,13 @@ import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController'; import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretsController from './secretsController'; import * as secretsController from './secretsController';
import * as environmentController from './environmentController';
export { export {
workspaceController, workspaceController,
serviceTokenDataController, serviceTokenDataController,
apiKeyDataController, apiKeyDataController,
secretController, secretController,
secretsController secretsController,
environmentController
} }
@@ -19,7 +19,6 @@ import {
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { postHogClient, EventService } from '../../services'; import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { ENV_SET } from '../../variables';
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
@@ -52,7 +51,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
// validate environment // validate environment
if (!ENV_SET.has(environment)) { const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
@@ -129,6 +129,11 @@ export const pullSecrets = async (req: Request, res: Response) => {
} else if (req.serviceTokenData) { } else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id userId = req.serviceTokenData.user._id
} }
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
secrets = await pull({ secrets = await pull({
userId, userId,
-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
+4 -4
View File
@@ -7,8 +7,6 @@ import {
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services'; import { BotService } from '../services';
import { import {
ENV_DEV,
EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY
} from '../variables'; } from '../variables';
@@ -36,11 +34,13 @@ interface Update {
const handleOAuthExchangeHelper = async ({ const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration, integration,
code code,
environment
}: { }: {
workspaceId: string; workspaceId: string;
integration: string; integration: string;
code: string; code: string;
environment: string;
}) => { }) => {
let action; let action;
let integrationAuth; let integrationAuth;
@@ -102,9 +102,9 @@ const handleOAuthExchangeHelper = async ({
// initialize new integration after exchange // initialize new integration after exchange
await new Integration({ await new Integration({
workspace: workspaceId, workspace: workspaceId,
environment: ENV_DEV,
isActive: false, isActive: false,
app: null, app: null,
environment,
integration, integration,
integrationAuth: integrationAuth._id integrationAuth: integrationAuth._id
}).save(); }).save();
+1 -6
View File
@@ -1,9 +1,5 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
@@ -13,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;
@@ -32,7 +28,6 @@ const integrationSchema = new Schema<IIntegration>(
}, },
environment: { environment: {
type: String, type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true required: true
}, },
isActive: { isActive: {
-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 ISecret { export interface ISecret {
@@ -53,7 +49,6 @@ const secretSchema = new Schema<ISecret>(
}, },
environment: { environment: {
type: String, type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true required: true
}, },
secretKeyCiphertext: { secretKeyCiphertext: {
-4
View File
@@ -1,7 +1,4 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
// TODO: deprecate
export interface IServiceToken { export interface IServiceToken {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
@@ -33,7 +30,6 @@ const serviceTokenSchema = new Schema<IServiceToken>(
}, },
environment: { environment: {
type: String, type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true required: true
}, },
expiresAt: { expiresAt: {
+31 -1
View File
@@ -4,6 +4,10 @@ export interface IWorkspace {
_id: Types.ObjectId; _id: Types.ObjectId;
name: string; name: string;
organization: Types.ObjectId; organization: Types.ObjectId;
environments: Array<{
name: string;
slug: string;
}>;
} }
const workspaceSchema = new Schema<IWorkspace>({ const workspaceSchema = new Schema<IWorkspace>({
@@ -15,7 +19,33 @@ const workspaceSchema = new Schema<IWorkspace>({
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Organization', ref: 'Organization',
required: true required: true
} },
environments: {
type: [
{
name: String,
slug: String,
},
],
default: [
{
name: "development",
slug: "dev"
},
{
name: "test",
slug: "test"
},
{
name: "staging",
slug: "staging"
},
{
name: "production",
slug: "prod"
}
],
},
}); });
const Workspace = model<IWorkspace>('Workspace', workspaceSchema); const Workspace = model<IWorkspace>('Workspace', workspaceSchema);
+57
View File
@@ -0,0 +1,57 @@
import express, { Response, Request } from 'express';
const router = express.Router();
import { body, param } from 'express-validator';
import { environmentController } from '../../controllers/v2';
import {
requireAuth,
requireWorkspaceAuth,
validateRequest,
} from '../../middleware';
import { ADMIN, MEMBER } from '../../variables';
router.post(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
body('environmentName').exists().trim(),
validateRequest,
environmentController.createWorkspaceEnvironment
);
router.put(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
body('environmentName').exists().trim(),
body('oldEnvironmentSlug').exists().trim(),
validateRequest,
environmentController.renameWorkspaceEnvironment
);
router.delete(
'/:workspaceId/environments',
requireAuth({
acceptedAuthModes: ['jwt'],
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
}),
param('workspaceId').exists().trim(),
body('environmentSlug').exists().trim(),
validateRequest,
environmentController.deleteWorkspaceEnvironment
);
export default router;
+4 -2
View File
@@ -3,11 +3,13 @@ import secrets from './secrets';
import workspace from './workspace'; import workspace from './workspace';
import serviceTokenData from './serviceTokenData'; import serviceTokenData from './serviceTokenData';
import apiKeyData from './apiKeyData'; import apiKeyData from './apiKeyData';
import environment from "./environment"
export { export {
secret, secret,
secrets, secrets,
workspace, workspace,
serviceTokenData, serviceTokenData,
apiKeyData apiKeyData,
} environment
}
+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']
+6 -6
View File
@@ -11,10 +11,6 @@ import {
setIntegrationAuthAccessHelper, setIntegrationAuthAccessHelper,
} from '../helpers/integration'; } from '../helpers/integration';
import { exchangeCode } from '../integrations'; import { exchangeCode } from '../integrations';
import {
ENV_DEV,
EVENT_PUSH_SECRETS
} from '../variables';
// should sync stuff be here too? Probably. // should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService. // TODO: move bot functions to IntegrationService.
@@ -32,22 +28,26 @@ class IntegrationService {
* - Create bot sequence for integration * - Create bot sequence for integration
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - workspace environment
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.code - code * @param {String} obj.code - code
*/ */
static async handleOAuthExchange({ static async handleOAuthExchange({
workspaceId, workspaceId,
integration, integration,
code code,
environment
}: { }: {
workspaceId: string; workspaceId: string;
integration: string; integration: string;
code: string; code: string;
environment: string;
}) { }) {
await handleOAuthExchangeHelper({ await handleOAuthExchangeHelper({
workspaceId, workspaceId,
integration, integration,
code code,
environment
}); });
} }
+2 -2
View File
@@ -46,7 +46,7 @@ export default function ListBox({
> >
<div className="flex flex-row"> <div className="flex flex-row">
{text} {text}
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300"> <span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize">
{' '} {' '}
{selected} {selected}
</span> </span>
@@ -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 ? selectedServiceTokenEnv.slug : environments[0]?.name,
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 ? selectedServiceTokenEnv?.name : environments[0]?.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>
@@ -0,0 +1,145 @@
import { FormEventHandler, Fragment, useEffect, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import Button from '../buttons/Button';
import InputField from '../InputField';
type FormFields = { name: string; slug: string };
type Props = {
isOpen?: boolean;
isEditMode?: boolean;
// on edit mode load up initial values
initialValues?: FormFields;
onClose: () => 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 AddUpdateEnvironmentDialog = ({
isOpen,
onClose,
onCreateSubmit,
onEditSubmit,
initialValues,
isEditMode,
}: Props) => {
const [formInput, setFormInput] = useState<FormFields>({
name: '',
slug: '',
});
// This use effect can be removed when the unmount is happening from outside the component
// When unmount happens outside state gets unmounted also
useEffect(() => {
setFormInput(initialValues || { name: '', slug: '' });
}, [isOpen]);
// REFACTOR: Move to react-hook-form with yup for better form management
const onInputChange = (fieldName: string, fieldValue: string) => {
setFormInput((state) => ({ ...state, [fieldName]: fieldValue }));
};
const onFormSubmit: FormEventHandler = (e) => {
e.preventDefault();
const data = {
name: formInput.name.toLowerCase(),
slug: formInput.slug.toLowerCase(),
};
if (isEditMode) {
onEditSubmit(data);
return;
}
onCreateSubmit(data);
};
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative z-20' onClose={onClose}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-out duration-150'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-black bg-opacity-70' />
</Transition.Child>
<div className='fixed inset-0 overflow-y-auto z-50'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
{isEditMode
? 'Update environment'
: 'Create a new environment'}
</Dialog.Title>
<form onSubmit={onFormSubmit}>
<div className='max-h-28 mt-4'>
<InputField
label='Environment Name'
onChangeHandler={(val) => onInputChange('name', val)}
type='varName'
value={formInput.name}
placeholder=''
isRequired
// error={error.length > 0}
// errorText={error}
/>
</div>
<div className='max-h-28 mt-4'>
<InputField
label='Environment Slug'
onChangeHandler={(val) => onInputChange('slug', val)}
type='varName'
value={formInput.slug}
placeholder=''
isRequired
// error={error.length > 0}
// errorText={error}
/>
</div>
<p className='text-xs text-gray-500 mt-2'>
Slugs are shorthands used in cli to access environment
</p>
<div className='mt-4 max-w-min'>
<Button
onButtonPressed={() => null}
type='submit'
color='mineshaft'
text={isEditMode ? 'Update' : 'Create'}
active={formInput.name !== '' && formInput.slug !== ''}
size='md'
/>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
@@ -0,0 +1,104 @@
import { Fragment, useEffect, useState } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import InputField from '../InputField';
// REFACTOR: Move all these modals into one reusable one
type Props = {
isOpen?: boolean;
onClose: ()=>void;
title: string;
onSubmit:()=>void;
deleteKey?:string;
}
const DeleteActionModal = ({
isOpen,
onClose,
title,
onSubmit,
deleteKey
}:Props) => {
const [deleteInputField, setDeleteInputField] = useState("")
useEffect(() => {
setDeleteInputField("");
}, [isOpen]);
return (
<div>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative z-10' onClose={onClose}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-150'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<div className='fixed inset-0 bg-black bg-opacity-25' />
</Transition.Child>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
>
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
{title}
</Dialog.Title>
<div className='mt-2'>
<p className='text-sm text-gray-500'>
This action is irrevertible.
</p>
</div>
<div className='mt-2'>
<InputField
isRequired
label={`Type ${deleteKey} to delete the resource`}
onChangeHandler={(val) => setDeleteInputField(val)}
value={deleteInputField}
type='text'
/>
</div>
<div className='mt-6'>
<button
type='button'
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={onSubmit}
disabled={
Boolean(deleteKey) && deleteInputField !== deleteKey
}
>
Delete
</button>
<button
type='button'
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={onClose}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
export default DeleteActionModal;
@@ -0,0 +1,167 @@
import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
import { usePopUp } from '../../../hooks/usePopUp';
import Button from '../buttons/Button';
import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog';
import DeleteActionModal from '../dialog/DeleteActionModal';
type Env = { name: string; slug: string };
type Props = {
data: Env[];
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 (
<>
<div className='flex flex-row justify-between w-full'>
<div className='flex flex-col w-full'>
<p className='text-xl font-semibold mb-3'>Project Environments</p>
<p className='text-base text-gray-400 mb-4'>
Choose which environments will show up in your dashboard like
development, staging, production
</p>
<p className='text-sm mr-1 text-gray-500 self-start'>
Note: the text in slugs shows how these environmant should be
accessed in CLI.
</p>
</div>
<div className='w-48'>
<Button
text='Add New Env'
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
color='mineshaft'
icon={faPlus}
size='md'
/>
</div>
</div>
<div className='table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1'>
<div className='absolute rounded-t-md w-full h-12 bg-white/5'></div>
<table className='w-full my-1'>
<thead className='text-bunker-300'>
<tr>
<th className='text-left pl-6 pt-2.5 pb-2'>Name</th>
<th className='text-left pl-6 pt-2.5 pb-2'>Slug</th>
<th></th>
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data.map(({ name, slug }) => {
return (
<tr
key={name}
className='bg-bunker-800 hover:bg-bunker-800/5 duration-100'
>
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize'>
{name}
</td>
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300'>
{slug}
</td>
<td className='py-2 border-mineshaft-700 border-t flex'>
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
<Button
onButtonPressed={() =>
handlePopUpOpen('createUpdateEnv', { name, slug })
}
color='red'
size='icon-sm'
icon={faPencil}
/>
</div>
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center'>
<Button
onButtonPressed={() =>
handlePopUpOpen('deleteEnv', { name, slug })
}
color='red'
size='icon-sm'
icon={faX}
/>
</div>
</td>
</tr>
);
})
) : (
<tr>
<td
colSpan={4}
className='text-center pt-7 pb-4 text-bunker-400'
>
No environmants found
</td>
</tr>
)}
</tbody>
</table>
<DeleteActionModal
isOpen={popUp['deleteEnv'].isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
}?`}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
onClose={() => handlePopUpClose('deleteEnv')}
onSubmit={onEnvDeleteCB}
/>
<AddUpdateEnvironmentDialog
isOpen={popUp.createUpdateEnv.isOpen}
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
initialValues={popUp?.createUpdateEnv?.data as any}
onClose={() => handlePopUpClose('createUpdateEnv')}
onCreateSubmit={onEnvCreateCB}
onEditSubmit={onEnvUpdateCB}
/>
</div>
</>
);
};
export default EnvironmentTable;
@@ -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()}
@@ -74,7 +74,7 @@ const CloudIntegration = ({
integrationAuths integrationAuths
.map((authorization) => authorization.integration) .map((authorization) => authorization.integration)
.includes(cloudIntegrationOption.name.toLowerCase()) && ( .includes(cloudIntegrationOption.name.toLowerCase()) && (
<div className="absolute group z-50 top-0 right-0 flex flex-row"> <div className="absolute group z-40 top-0 right-0 flex flex-row">
<div <div
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
@@ -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 {
@@ -36,13 +34,23 @@ interface IntegrationApp {
siteId: string; siteId: string;
} }
const Integration = ({ type Props = {
integration
}: {
integration: Integration; integration: Integration;
}) => { environments: Array<{ name: string; slug: string }>;
const [integrationEnvironment, setIntegrationEnvironment] = useState( };
reverseEnvMapping[integration.environment]
const Integration = ({
integration,
environments = []
}:Props ) => {
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<
Props['environments'][0]
>(
environments.find(({ slug }) => slug === integration.environment) || {
name: '',
slug: '',
}
); );
const [fileState, setFileState] = useState([]); const [fileState, setFileState] = useState([]);
const router = useRouter(); const router = useRouter();
@@ -93,7 +101,7 @@ const Integration = ({
case "vercel": case "vercel":
return ( return (
<div> <div>
<div className="text-gray-400 text-xs font-semibold mb-2"> <div className="text-gray-400 text-xs font-semibold mb-2 w-60">
ENVIRONMENT ENVIRONMENT
</div> </div>
<ListBox <ListBox
@@ -104,6 +112,7 @@ const Integration = ({
] : null} ] : null}
selected={integrationTarget} selected={integrationTarget}
onChange={setIntegrationTarget} onChange={setIntegrationTarget}
isFull={true}
/> />
</div> </div>
); );
@@ -136,42 +145,47 @@ const Integration = ({
if (!integrationApp || apps.length === 0) return <div></div> if (!integrationApp || apps.length === 0) return <div></div>
return ( return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between"> <div className='max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between'>
<div className="flex"> <div className='flex'>
<div> <div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p> <p className='text-gray-400 text-xs font-semibold mb-2'>
<ListBox data={!integration.isActive ? [ ENVIRONMENT
"Development", </p>
"Staging", <ListBox
"Testing", data={
"Production", !integration.isActive
] : null} ? environments.map(({ name }) => name)
selected={integrationEnvironment} : null
onChange={(environment) => { }
setIntegrationEnvironment(environment); selected={integrationEnvironment.name}
}} onChange={(envName) =>
setIntegrationEnvironment(
environments.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
}
)
}
isFull={true} isFull={true}
/> />
</div> </div>
<div className="pt-2"> <div className='pt-2'>
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowRight} icon={faArrowRight}
className="mx-4 text-gray-400 mt-8" className='mx-4 text-gray-400 mt-8'
/> />
</div> </div>
<div className="mr-2"> <div className='mr-2'>
<p className="text-gray-400 text-xs font-semibold mb-2"> <p className='text-gray-400 text-xs font-semibold mb-2'>
INTEGRATION INTEGRATION
</p> </p>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300"> <div className='py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300'>
{integration.integration.charAt(0).toUpperCase() + {integration.integration.charAt(0).toUpperCase() +
integration.integration.slice(1)} integration.integration.slice(1)}
</div> </div>
</div> </div>
<div className="mr-2"> <div className='mr-2'>
<div className="text-gray-400 text-xs font-semibold mb-2"> <div className='text-gray-400 text-xs font-semibold mb-2'>APP</div>
APP
</div>
<ListBox <ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null} data={!integration.isActive ? apps.map((app) => app.name) : null}
selected={integrationApp} selected={integrationApp}
@@ -182,52 +196,55 @@ const Integration = ({
</div> </div>
{renderIntegrationSpecificParams(integration)} {renderIntegrationSpecificParams(integration)}
</div> </div>
<div className="flex items-end"> <div className='flex items-end'>
{integration.isActive ? ( {integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4"> <div className='max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4'>
<FontAwesomeIcon <FontAwesomeIcon
icon={faRotate} icon={faRotate}
className="text-lg mr-2.5 text-primary animate-spin" className='text-lg mr-2.5 text-primary animate-spin'
/>
<div className="text-gray-300 font-semibold">In Sync</div>
</div>
) : (
<Button
text="Start Integration"
onButtonPressed={async () => {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const siteId = siteApp?.siteId ? siteApp.siteId : null;
const result = await updateIntegration({
integrationId: integration._id,
environment: envMapping[integrationEnvironment],
app: integrationApp,
isActive: true,
target: integrationTarget ? integrationTarget.toLowerCase() : null,
context: integrationContext ? reverseContextNetlifyMapping[integrationContext] : null,
siteId
});
router.reload();
}}
color="mineshaft"
size="md"
/>
)}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
<Button
onButtonPressed={async () => {
await deleteIntegration({
integrationId: integration._id,
});
router.reload();
}}
color="red"
size="icon-md"
icon={faX}
/> />
<div className='text-gray-300 font-semibold'>In Sync</div>
</div> </div>
) : (
<Button
text='Start Integration'
onButtonPressed={async () => {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const siteId = siteApp?.siteId ? siteApp.siteId : null;
await updateIntegration({
integrationId: integration._id,
environment: integrationEnvironment.slug,
app: integrationApp,
isActive: true,
target: integrationTarget
? integrationTarget.toLowerCase()
: null,
context: integrationContext
? reverseContextNetlifyMapping[integrationContext]
: null,
siteId,
});
router.reload();
}}
color='mineshaft'
size='md'
/>
)}
<div className='opacity-50 hover:opacity-100 duration-200 ml-2'>
<Button
onButtonPressed={async () => {
await deleteIntegration({
integrationId: integration._id,
});
router.reload();
}}
color='red'
size='icon-md'
icon={faX}
/>
</div>
</div> </div>
</div> </div>
); );
@@ -5,7 +5,8 @@ import guidGenerator from "~/utilities/randomId";
import Integration from "./Integration"; import Integration from "./Integration";
interface Props { interface Props {
integrations: any integrations: any;
environments: Array<{ name: string; slug: string }>;
} }
interface IntegrationType { interface IntegrationType {
@@ -19,7 +20,8 @@ interface IntegrationType {
} }
const ProjectIntegrationSection = ({ const ProjectIntegrationSection = ({
integrations integrations,
environments = [],
}: Props) => { }: Props) => {
return integrations.length > 0 ? ( return integrations.length > 0 ? (
<div className="mb-12"> <div className="mb-12">
@@ -33,6 +35,7 @@ const ProjectIntegrationSection = ({
<Integration <Integration
key={guidGenerator()} key={guidGenerator()}
integration={integration} integration={integration}
environments={environments}
/> />
))} ))}
</div> </div>
@@ -32,7 +32,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');
} }
+1
View File
@@ -0,0 +1 @@
export { usePopUp } from './usePopUp';
+69
View File
@@ -0,0 +1,69 @@
import { useCallback, useState } from 'react';
interface usePopUpProps {
name: Readonly<string>;
isOpen: boolean;
}
/**
* to provide better intellisense
* checks which type of inputProps were given and converts them into key-names
* SIDENOTE: On inputting give it as const and not string with (as const)
*/
type usePopUpState<T extends Readonly<string[]> | usePopUpProps[]> = {
[P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: {
isOpen: boolean;
data?: unknown;
};
};
interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
popUp: usePopUpState<T>;
handlePopUpOpen: (popUpName: keyof usePopUpState<T>, data?: unknown) => void;
handlePopUpClose: (popUpName: keyof usePopUpState<T>) => void;
handlePopUpToggle: (popUpName: keyof usePopUpState<T>) => void;
}
/**
* This hook is used to manage multiple popUps/modal/dialog in a page
* Provides api to open,close,toggle and also store temporary data for the popUp
* @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}]
*/
export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>(
popUpNames: T
): usePopUpReturn<T> => {
const [popUp, setPopUp] = useState<usePopUpState<T>>(
Object.fromEntries(
popUpNames.map((popUpName) =>
typeof popUpName === 'string'
? [popUpName, { isOpen: false }]
: [popUpName.name, { isOpen: popUpName.isOpen }]
) // convert into an array of [[popUpName,state]] then into Object
) as usePopUpState<T> // to override generic string return type of the function
);
const handlePopUpOpen = useCallback(
(popUpName: keyof usePopUpState<T>, data?: unknown) => {
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } }));
},
[]
);
const handlePopUpClose = useCallback((popUpName: keyof usePopUpState<T>) => {
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } }));
}, []);
const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => {
setPopUp((popUp) => ({
...popUp,
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
}));
}, []);
return {
popUp,
handlePopUpOpen,
handlePopUpClose,
handlePopUpToggle,
};
};
@@ -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;
@@ -0,0 +1,31 @@
import SecurityClient from '~/utilities/SecurityClient';
interface Workspace {
__v: number;
_id: string;
name: string;
organization: string;
environments: Array<{ name: string; slug: string }>;
}
/**
* This route lets us get the workspaces of a certain user
* @returns
*/
const getAWorkspace = (workspaceID:string) => {
return SecurityClient.fetchCall(`/api/v1/workspace/${workspaceID}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}).then(async (res) => {
if (res?.status == 200) {
const data = (await res.json()) as unknown as { workspace: Workspace };
return data.workspace;
}
throw new Error('Failed to get workspace');
});
};
export default getAWorkspace;
@@ -5,6 +5,7 @@ interface Workspace {
_id: string; _id: string;
name: string; name: string;
organization: string; organization: string;
environments: Array<{name:string, slug:string}>
} }
/** /**
+261 -199
View File
@@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { import {
faArrowDownAZ, faArrowDownAZ,
faArrowDownZA, faArrowDownZA,
@@ -34,7 +34,6 @@ import getSecretsForProject from '~/components/utilities/secrets/getSecretsForPr
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps'; import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
import guidGenerator from '~/utilities/randomId'; import guidGenerator from '~/utilities/randomId';
import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants';
import addSecrets from '../api/files/AddSecrets'; import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets'; import deleteSecrets from '../api/files/DeleteSecrets';
import updateSecrets from '../api/files/UpdateSecrets'; import updateSecrets from '../api/files/UpdateSecrets';
@@ -43,6 +42,10 @@ import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction'; import registerUserAction from '../api/userActions/registerUserAction';
import getWorkspaces from '../api/workspace/getWorkspaces'; import getWorkspaces from '../api/workspace/getWorkspaces';
type WorkspaceEnv = {
name: string;
slug: string;
};
interface SecretDataProps { interface SecretDataProps {
pos: number; pos: number;
@@ -104,11 +107,8 @@ export default function Dashboard() {
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]); const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
const [buttonReady, setButtonReady] = useState(false); const [buttonReady, setButtonReady] = useState(false);
const router = useRouter(); const router = useRouter();
const [workspaceId, setWorkspaceId] = useState('');
const [blurred, setBlurred] = useState(true); const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true); const [isKeyAvailable, setIsKeyAvailable] = useState(true);
const [env, setEnv] = useState('Development');
const [snapshotEnv, setSnapshotEnv] = useState('Development');
const [isNew, setIsNew] = useState(false); const [isNew, setIsNew] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [searchKeys, setSearchKeys] = useState(''); const [searchKeys, setSearchKeys] = useState('');
@@ -116,7 +116,7 @@ export default function Dashboard() {
const [sortMethod, setSortMethod] = useState('alphabetical'); const [sortMethod, setSortMethod] = useState('alphabetical');
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
const [hasUserEverPushed, setHasUserEverPushed] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
const [sidebarSecretId, toggleSidebar] = useState("None"); const [sidebarSecretId, toggleSidebar] = useState('None');
const [PITSidebarOpen, togglePITSidebar] = useState(false); const [PITSidebarOpen, togglePITSidebar] = useState(false);
const [sharedToHide, setSharedToHide] = useState<string[]>([]); const [sharedToHide, setSharedToHide] = useState<string[]>([]);
const [snapshotData, setSnapshotData] = useState<SnapshotProps>(); const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
@@ -126,6 +126,16 @@ export default function Dashboard() {
const { t } = useTranslation(); const { t } = useTranslation();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const workspaceId = router.query.id as string;
const [workspaceEnvs, setWorkspaceEnvs] = useState<WorkspaceEnv[]>([]);
const [selectedSnapshotEnv, setSelectedSnapshotEnv] =
useState<WorkspaceEnv>();
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
name: '',
slug: '',
});
// #TODO: fix save message for changing reroutes // #TODO: fix save message for changing reroutes
// const beforeRouteHandler = (url) => { // const beforeRouteHandler = (url) => {
// const warningText = // const warningText =
@@ -172,25 +182,37 @@ export default function Dashboard() {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) }) const tempNumSnapshots = await getProjectSercetSnapshotsCount({
workspaceId,
});
setNumSnapshots(tempNumSnapshots); setNumSnapshots(tempNumSnapshots);
const userWorkspaces = await getWorkspaces(); const userWorkspaces = await getWorkspaces();
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id); const workspace = userWorkspaces.find(
if ( (workspace) => workspace._id === workspaceId
!listWorkspaces.includes(router.asPath.split('/')[2]) );
) { if (!workspace) {
router.push('/dashboard/' + listWorkspaces[0]); 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(); const user = await getUser();
setIsNew( setIsNew(
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3 (Date.parse(String(new Date())) - Date.parse(user.createdAt)) /
60000 <
3
? true ? true
: false : false
); );
const userAction = await checkUserAction({ const userAction = await checkUserAction({
action: 'first_time_secrets_pushed' action: 'first_time_secrets_pushed',
}); });
setHasUserEverPushed(userAction ? true : false); setHasUserEverPushed(userAction ? true : false);
} catch (error) { } catch (error) {
@@ -205,25 +227,26 @@ export default function Dashboard() {
try { try {
setIsLoading(true); setIsLoading(true);
setBlurred(true); setBlurred(true);
setWorkspaceId(String(router.query.id)); // ENV
const dataToSort = await getSecretsForProject({ const dataToSort = await getSecretsForProject({
env, env: selectedEnv.slug,
setIsKeyAvailable, setIsKeyAvailable,
setData, setData,
workspaceId: String(router.query.id) workspaceId,
}); });
setInitialData(dataToSort); setInitialData(dataToSort);
reorderRows(dataToSort); reorderRows(dataToSort);
setIsLoading(false); setTimeout(
() => setIsLoading(false)
, 700);
} catch (error) { } catch (error) {
console.log('Error', error); console.log('Error', error);
setData(undefined); setData(undefined);
} }
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [env]); }, [selectedEnv]);
const addRow = () => { const addRow = () => {
setIsNew(false); setIsNew(false);
@@ -237,37 +260,22 @@ export default function Dashboard() {
value: '', value: '',
valueOverride: undefined, valueOverride: undefined,
comment: '', comment: '',
} },
]); ]);
}; };
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
setButtonReady(true); setButtonReady(true);
toggleSidebar("None"); toggleSidebar('None');
createNotification({ createNotification({
text: `${secretName} has been deleted. Remember to save changes.`, 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'
/** );
* This function deleted the override of a certain secrer
* @param {string} id - id of a shared secret; the override with the same key should be deleted
*/
const deleteOverride = (id: string) => {
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;
// change the sidebar to this shared secret; and unhide it
// 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 modifyValue = (value: string, pos: number) => { const modifyValue = (value: string, pos: number) => {
@@ -341,14 +349,14 @@ export default function Dashboard() {
if (nameErrors) { if (nameErrors) {
return createNotification({ return createNotification({
text: 'Solve all name errors before saving secrets.', text: 'Solve all name errors before saving secrets.',
type: 'error' type: 'error',
}); });
} }
if (duplicatesExist) { if (duplicatesExist) {
return createNotification({ return createNotification({
text: 'Remove duplicated secret names before saving.', text: 'Remove duplicated secret names before saving.',
type: 'error' type: 'error',
}); });
} }
@@ -404,11 +412,11 @@ export default function Dashboard() {
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) }); await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
} }
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) { if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: envMapping[env] }); const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: selectedEnv.slug });
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId }); secrets && await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
} }
if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) { if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), workspaceId, env: envMapping[env] }); const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), workspaceId, env: selectedEnv.slug });
secrets && await updateSecrets({ secrets }); secrets && await updateSecrets({ secrets });
} }
@@ -434,36 +442,49 @@ export default function Dashboard() {
setBlurred(!blurred); setBlurred(!blurred);
}; };
const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => { const sortValuesHandler = (
const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod; dataToSort: SecretDataProps[] | 1,
specificSortMethod?: 'alphabetical' | '-alphabetical'
) => {
const howToSort =
specificSortMethod == undefined ? sortMethod : specificSortMethod;
const sortedData = (dataToSort != 1 ? dataToSort : data)! const sortedData = (dataToSort != 1 ? dataToSort : data)!
.sort((a, b) => .sort((a, b) =>
howToSort == 'alphabetical' howToSort == 'alphabetical'
? a.key.localeCompare(b.key) ? a.key.localeCompare(b.key)
: b.key.localeCompare(a.key) : b.key.localeCompare(a.key)
) )
.map((item: SecretDataProps, index: number) => { .map((item: SecretDataProps, index: number) => {
return { return {
...item, ...item,
pos: index pos: index,
}; };
}); });
setData(sortedData); setData(sortedData);
}; };
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => { const deleteCertainRow = ({
deleteRow({ids, secretName}); ids,
secretName,
}: {
ids: string[];
secretName: string;
}) => {
deleteRow({ ids, secretName });
}; };
return data ? ( return data ? (
<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>
<title>{t("common:head-title", { title: t("dashboard:title") })}</title> <title>{t('common:head-title', { title: t('dashboard:title') })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel='icon' href='/infisical.ico' />
<meta property="og:image" content="/images/message.png" /> <meta property='og:image' content='/images/message.png' />
<meta property="og:title" content={String(t("dashboard:og-title"))} /> <meta property='og:title' content={String(t('dashboard:og-title'))} />
<meta name="og:description" content={String(t("dashboard:og-description"))} /> <meta
name='og:description'
content={String(t('dashboard:og-description'))}
/>
</Head> </Head>
<div className="flex flex-row"> <div className="flex flex-row">
{sidebarSecretId != "None" && <SideBar {sidebarSecretId != "None" && <SideBar
@@ -488,56 +509,68 @@ export default function Dashboard() {
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} /> <NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
{checkDocsPopUpVisible && ( {checkDocsPopUpVisible && (
<BottonRightPopup <BottonRightPopup
buttonText={t("dashboard:check-docs.button")} buttonText={t('dashboard:check-docs.button')}
buttonLink="https://infisical.com/docs/getting-started/introduction" buttonLink='https://infisical.com/docs/getting-started/introduction'
titleText={t("dashboard:check-docs.title")} titleText={t('dashboard:check-docs.title')}
emoji="🎉" emoji='🎉'
textLine1={t("dashboard:check-docs.line1")} textLine1={t('dashboard:check-docs.line1')}
textLine2={t("dashboard:check-docs.line2")} textLine2={t('dashboard:check-docs.line2')}
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible} setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
/> />
)} )}
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl"> <div className='flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl'>
{snapshotData && {snapshotData && (
<div className={`flex justify-start max-w-sm mt-1 mr-2`}> <div className={`flex justify-start max-w-sm mt-1 mr-2`}>
<Button <Button
text={String(t("Go back to current"))} text={String(t('Go back to current'))}
onButtonPressed={() => setSnapshotData(undefined)} onButtonPressed={() => setSnapshotData(undefined)}
color="mineshaft" color='mineshaft'
size="md" size='md'
icon={faArrowLeft} icon={faArrowLeft}
/> />
</div>} </div>
<div className="flex flex-row justify-start items-center text-3xl"> )}
<div className="font-semibold mr-4 mt-1 flex flex-row items-center"> <div className='flex flex-row justify-start items-center text-3xl'>
<p>{snapshotData ? "Secret Snapshot" : t("dashboard:title")}</p> <div className='font-semibold mr-4 mt-1 flex flex-row items-center'>
{snapshotData && <span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>{new Date(snapshotData.createdAt).toLocaleString()}</span>} <p>{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}</p>
{snapshotData && (
<span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>
{new Date(snapshotData.createdAt).toLocaleString()}
</span>
)}
</div> </div>
{!snapshotData && data?.length == 0 && ( {!snapshotData && data?.length == 0 && (
<ListBox <ListBox
selected={env} selected={selectedEnv.name}
data={['Development', 'Staging', 'Production', 'Testing']} data={workspaceEnvs.map(({ name }) => name)}
onChange={setEnv} onChange={(envName) =>
setSelectedEnv(
workspaceEnvs.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown',
}
)
}
/> />
)} )}
</div> </div>
<div className="flex flex-row"> <div className='flex flex-row'>
<div className={`flex justify-start max-w-sm mt-1 mr-2`}> <div className={`flex justify-start max-w-sm mt-1 mr-2`}>
<Button <Button
text={String(numSnapshots + " " + t("Commits"))} text={String(numSnapshots + ' ' + t('Commits'))}
onButtonPressed={() => togglePITSidebar(true)} onButtonPressed={() => togglePITSidebar(true)}
color="mineshaft" color='mineshaft'
size="md" size='md'
icon={faClockRotateLeft} icon={faClockRotateLeft}
/> />
</div> </div>
{(data?.length !== 0 || buttonReady) && !snapshotData && ( {(data?.length !== 0 || buttonReady) && !snapshotData && (
<div className={`flex justify-start max-w-sm mt-1`}> <div className={`flex justify-start max-w-sm mt-1`}>
<Button <Button
text={String(t("common:save-changes"))} text={String(t('common:save-changes'))}
onButtonPressed={savePush} onButtonPressed={savePush}
color="primary" color='primary'
size="md" size='md'
active={buttonReady} active={buttonReady}
iconDisabled={faCheck} iconDisabled={faCheck}
textDisabled={String(t("common:saved"))} textDisabled={String(t("common:saved"))}
@@ -551,7 +584,7 @@ export default function Dashboard() {
onButtonPressed={async () => { onButtonPressed={async () => {
// Update secrets in the state only for the current environment // Update secrets in the state only for the current environment
const rolledBackSecrets = snapshotData.secretVersions const rolledBackSecrets = snapshotData.secretVersions
.filter(row => reverseEnvMapping[row.environment] == env) .filter(row => row.environment == selectedEnv.slug)
.map((sv, position) => { .map((sv, position) => {
return { return {
id: sv.id, idOverride: sv.id, pos: position, valueOverride: sv.valueOverride, key: sv.key, value: sv.value, comment: '' id: sv.id, idOverride: sv.id, pos: position, valueOverride: sv.valueOverride, key: sv.key, value: sv.value, comment: ''
@@ -575,88 +608,116 @@ export default function Dashboard() {
</div>} </div>}
</div> </div>
</div> </div>
<div className="mx-6 w-full pr-12"> <div className='mx-6 w-full pr-12'>
<div className="flex flex-col max-w-5xl pb-1"> <div className='flex flex-col max-w-5xl pb-1'>
<div className="w-full flex flex-row items-start"> <div className='w-full flex flex-row items-start'>
{(snapshotData || data?.length !== 0) && ( {(snapshotData || data?.length !== 0) && (
<> <>
{!snapshotData {!snapshotData ? (
? <ListBox <ListBox
selected={env} selected={selectedEnv.name}
data={['Development', 'Staging', 'Production', 'Testing']} data={workspaceEnvs.map(({ name }) => name)}
onChange={setEnv} onChange={(envName) =>
/> setSelectedEnv(
: <ListBox workspaceEnvs.find(
selected={snapshotEnv} ({ name }) => envName === name
data={['Development', 'Staging', 'Production', 'Testing']} ) || {
onChange={setSnapshotEnv} name: 'unknown',
/>} slug: 'unknown',
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center"> }
)
}
/>
) : (
<ListBox
selected={selectedSnapshotEnv?.name || ''}
data={workspaceEnvs.map(({ name }) => name)}
onChange={(envName) =>
setSelectedSnapshotEnv(
workspaceEnvs.find(
({ name }) => envName === name
) || {
name: 'unknown',
slug: 'unknown',
}
)
}
/>
)}
<div className='h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center'>
<FontAwesomeIcon <FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400" className='bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400'
icon={faMagnifyingGlass} icon={faMagnifyingGlass}
/> />
<input <input
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none" className='pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none'
value={searchKeys} value={searchKeys}
onChange={(e) => setSearchKeys(e.target.value)} onChange={(e) => setSearchKeys(e.target.value)}
placeholder={String(t("dashboard:search-keys"))} placeholder={String(t('dashboard:search-keys'))}
/> />
</div> </div>
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start"> {!snapshotData && (
<Button <div className='ml-2 min-w-max flex flex-row items-start justify-start'>
onButtonPressed={() => reorderRows(1)} <Button
color="mineshaft" onButtonPressed={() => reorderRows(1)}
size="icon-md" color='mineshaft'
icon={ size='icon-md'
sortMethod == 'alphabetical' icon={
? faArrowDownAZ sortMethod == 'alphabetical'
: faArrowDownZA ? faArrowDownAZ
} : faArrowDownZA
/> }
</div>} />
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start"> </div>
<DownloadSecretMenu data={data} env={env} /> )}
</div>} {!snapshotData && (
<div className="ml-2 min-w-max flex flex-row items-start justify-start"> <div className='ml-2 min-w-max flex flex-row items-start justify-start'>
<DownloadSecretMenu
data={data}
env={selectedEnv.slug}
/>
</div>
)}
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
<Button <Button
onButtonPressed={changeBlurred} onButtonPressed={changeBlurred}
color="mineshaft" color='mineshaft'
size="icon-md" size='icon-md'
icon={blurred ? faEye : faEyeSlash} icon={blurred ? faEye : faEyeSlash}
/> />
</div> </div>
{!snapshotData && <div className="relative ml-2 min-w-max flex flex-row items-start justify-end"> {!snapshotData && (
<Button <div className='relative ml-2 min-w-max flex flex-row items-start justify-end'>
text={String(t("dashboard:add-key"))} <Button
onButtonPressed={addRow} text={String(t('dashboard:add-key'))}
color="mineshaft" onButtonPressed={addRow}
icon={faPlus} color='mineshaft'
size="md" icon={faPlus}
/> size='md'
{isNew && ( />
<span className="absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4"> {isNew && (
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75 h-4 w-4"></span> <span className='absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4'>
<span className="relative inline-flex rounded-full h-3 w-3 bg-primary"></span> <span className='animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75 h-4 w-4'></span>
</span> <span className='relative inline-flex rounded-full h-3 w-3 bg-primary'></span>
)} </span>
</div>} )}
</div>
)}
</> </>
)} )}
</div> </div>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center h-full my-48"> <div className='flex items-center justify-center h-full my-48'>
<Image <Image
src="/images/loading/loading.gif" src='/images/loading/loading.gif'
height={60} height={60}
width={100} width={100}
alt="infisical loading indicator" alt='infisical loading indicator'
></Image> ></Image>
</div> </div>
) : ( ) : data?.length !== 0 ? (
data?.length !== 0 ? ( <div className='flex flex-col w-full mt-1 mb-2'>
<div className="flex flex-col w-full mt-1 mb-2">
<div <div
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`} className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
> >
@@ -679,7 +740,7 @@ export default function Dashboard() {
/> />
))} ))}
{snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key)) {snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key))
.filter(row => reverseEnvMapping[row.environment] == snapshotEnv) .filter(row => row.environment == selectedSnapshotEnv?.slug)
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase())) .filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
.filter( .filter(
row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
@@ -707,21 +768,23 @@ export default function Dashboard() {
/> />
))} ))}
</div> </div>
{!snapshotData && <div className="w-full max-w-5xl px-2 pt-3"> {!snapshotData && (
<DropZone <div className='w-full max-w-5xl px-2 pt-3'>
setData={addData} <DropZone
setErrorDragAndDrop={setErrorDragAndDrop} setData={addData}
createNewFile={addRow} setErrorDragAndDrop={setErrorDragAndDrop}
errorDragAndDrop={errorDragAndDrop} createNewFile={addRow}
setButtonReady={setButtonReady} errorDragAndDrop={errorDragAndDrop}
keysExist={true} setButtonReady={setButtonReady}
numCurrentRows={data.length} keysExist={true}
/> numCurrentRows={data.length}
</div>} />
</div>
)}
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28"> <div className='flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28'>
{isKeyAvailable && !snapshotData && ( {isKeyAvailable && !snapshotData && (
<DropZone <DropZone
setData={setData} setData={setData}
@@ -733,36 +796,35 @@ export default function Dashboard() {
keysExist={false} keysExist={false}
/> />
)} )}
{ {!isKeyAvailable && (
(!isKeyAvailable && ( <>
<> <FontAwesomeIcon
<FontAwesomeIcon className='text-7xl mt-20 mb-8'
className="text-7xl mt-20 mb-8" icon={faFolderOpen}
icon={faFolderOpen} />
/> <p>
<p> To view this file, contact your administrator for
To view this file, contact your administrator for permission.
permission. </p>
</p> <p className='mt-1'>
<p className="mt-1"> They need to grant you access in the team tab.
They need to grant you access in the team tab. </p>
</p> </>
</> )}
))}
</div> </div>
))} )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center"> <div className='relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center'>
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div> <div className='absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full'></div>
<Image <Image
src="/images/loading/loading.gif" src='/images/loading/loading.gif'
height={70} height={70}
width={120} width={120}
alt="loading animation" alt='loading animation'
></Image> ></Image>
</div> </div>
); );
@@ -770,4 +832,4 @@ export default function Dashboard() {
Dashboard.requireAuth = true; Dashboard.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(["dashboard"]); export const getServerSideProps = getTranslatedServerSideProps(['dashboard']);
+11 -9
View File
@@ -24,6 +24,7 @@ import setBotActiveStatus from "../api/bot/setBotActiveStatus";
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions"; import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations"; import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations"; import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
import getAWorkspace from "../api/workspace/getAWorkspace";
import getLatestFileKey from "../api/workspace/getLatestFileKey"; import getLatestFileKey from "../api/workspace/getLatestFileKey";
const { const {
decryptAssymmetric, decryptAssymmetric,
@@ -34,6 +35,7 @@ const crypto = require("crypto");
export default function Integrations() { export default function Integrations() {
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]); const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
const [integrationAuths, setIntegrationAuths] = useState([]); const [integrationAuths, setIntegrationAuths] = useState([]);
const [environments,setEnvironments] = useState([])
const [integrations, setIntegrations] = useState([]); const [integrations, setIntegrations] = useState([]);
const [bot, setBot] = useState(null); const [bot, setBot] = useState(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false); const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
@@ -41,11 +43,15 @@ export default function Integrations() {
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null); const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
const router = useRouter(); const router = useRouter();
const workspaceId = router.query.id;
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(async () => { useEffect(async () => {
try { try {
const workspace = await getAWorkspace(workspaceId);
setEnvironments(workspace.environments);
// get cloud integration options // get cloud integration options
setCloudIntegrationOptions( setCloudIntegrationOptions(
await getIntegrationOptions() await getIntegrationOptions()
@@ -54,23 +60,19 @@ export default function Integrations() {
// get project integration authorizations // get project integration authorizations
setIntegrationAuths( setIntegrationAuths(
await getWorkspaceAuthorizations({ await getWorkspaceAuthorizations({
workspaceId: router.query.id, workspaceId
}) })
); );
// get project integrations // get project integrations
setIntegrations( setIntegrations(
await getWorkspaceIntegrations({ await getWorkspaceIntegrations({
workspaceId: router.query.id, workspaceId,
}) })
); );
// get project bot // get project bot
setBot( setBot(await getBot({ workspaceId }));
await getBot({
workspaceId: router.query.id
}
));
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -90,7 +92,7 @@ export default function Integrations() {
if (bot) { if (bot) {
// case: there is a bot // case: there is a bot
const key = await getLatestFileKey({ workspaceId: router.query.id }); const key = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const WORKSPACE_KEY = decryptAssymmetric({ const WORKSPACE_KEY = decryptAssymmetric({
@@ -214,7 +216,7 @@ export default function Integrations() {
handleBotActivate={handleBotActivate} handleBotActivate={handleBotActivate}
handleIntegrationOption={handleIntegrationOption} handleIntegrationOption={handleIntegrationOption}
/> */} /> */}
<IntegrationSection integrations={integrations} /> <IntegrationSection integrations={integrations} environments={environments} />
{(cloudIntegrationOptions.length > 0 && bot) ? ( {(cloudIntegrationOptions.length > 0 && bot) ? (
<CloudIntegrationSection <CloudIntegrationSection
cloudIntegrationOptions={cloudIntegrationOptions} cloudIntegrationOptions={cloudIntegrationOptions}
-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",
]);
+358
View File
@@ -0,0 +1,358 @@
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 deleteEnvironment from '~/pages/api/environments/deleteEnvironment';
import updateEnvironment from '~/pages/api/environments/updateEnvironment';
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
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 [serviceTokens, setServiceTokens] = useState([]);
const [environments, setEnvironments] = useState<Array<EnvData>>([]);
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
const [isAddOpen, setIsAddOpen] = useState(false);
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] =
useState(false);
const [projectIdCopied, setProjectIdCopied] = useState(false);
const workspaceId = router.query.id as string;
const { t } = useTranslation();
/**
* 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: string) => {
setButtonReady(true);
setWorkspaceName(newName);
};
const submitChanges = (newWorkspaceName: string) => {
renameWorkspace(workspaceId, newWorkspaceName);
setButtonReady(false);
};
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 () => {
const userWorkspaces = await getWorkspaces();
if (userWorkspaces.length > 1) {
if (
userWorkspaces.filter(
(workspace) => workspace._id === workspaceId
)[0].name == workspaceToBeDeletedName
) {
await deleteWorkspace(workspaceId);
const userWorkspaces = await getWorkspaces();
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 (
<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={workspaceId}
environments={environments}
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
label=''
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') as string}
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 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-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-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') as string}
onButtonPressed={() => {
setIsAddServiceTokenDialogOpen(true);
}}
color='mineshaft'
icon={faPlus}
size='md'
/>
</div>
</div>
<ServiceTokenTable
data={serviceTokens}
workspaceName={workspaceName}
setServiceTokens={setServiceTokens as any}
/>
</div>
</div>
</div>
<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'>
{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',
]);
+1
View File
@@ -3,6 +3,7 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/components/*": ["components/*"], "~/components/*": ["components/*"],
"~/hooks/*": ["hooks/*"],
"~/utilities/*": ["components/utilities/*"], "~/utilities/*": ["components/utilities/*"],
"~/*": ["const"], "~/*": ["const"],
"~/pages/*": ["pages/*"] "~/pages/*": ["pages/*"]