mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Merge pull request #186 from akhilmhdh/feat/#31
feat(#31): implemented api for environment crud operations
This commit is contained in:
@ -46,6 +46,7 @@ import {
|
||||
workspace as v2WorkspaceRouter,
|
||||
serviceTokenData as v2ServiceTokenDataRouter,
|
||||
apiKeyData as v2APIKeyDataRouter,
|
||||
environment as v2EnvironmentRouter,
|
||||
} from './routes/v2';
|
||||
|
||||
import { healthCheck } from './routes/status';
|
||||
@ -103,6 +104,7 @@ app.use('/api/v1/integration', v1IntegrationRouter);
|
||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
|
||||
// v2 routes
|
||||
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
||||
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
||||
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||
|
@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { readFileSync } from 'fs';
|
||||
import { IntegrationAuth, Integration } from '../../models';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||
import { IntegrationService } from '../../services';
|
||||
import { getApps, revokeAccess } from '../../integrations';
|
||||
|
||||
@ -31,11 +31,17 @@ export const oAuthExchange = async (
|
||||
|
||||
if (!INTEGRATION_SET.has(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({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment: environments[0].slug,
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { EventService } from '../../services';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { postHogClient } from '../../services';
|
||||
|
||||
interface PushSecret {
|
||||
@ -44,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
@ -116,7 +116,8 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
@ -183,7 +184,8 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ServiceToken } from '../../models';
|
||||
import { createToken } from '../../helpers/auth';
|
||||
import { ENV_SET } from '../../variables';
|
||||
import { JWT_SERVICE_SECRET } from '../../config';
|
||||
|
||||
/**
|
||||
@ -36,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
||||
} = req.body;
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
|
204
backend/src/controllers/v2/environmentController.ts
Normal file
204
backend/src/controllers/v2/environmentController.ts
Normal file
@ -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,11 +3,13 @@ import * as serviceTokenDataController from './serviceTokenDataController';
|
||||
import * as apiKeyDataController from './apiKeyDataController';
|
||||
import * as secretController from './secretController';
|
||||
import * as secretsController from './secretsController';
|
||||
import * as environmentController from './environmentController';
|
||||
|
||||
export {
|
||||
workspaceController,
|
||||
serviceTokenDataController,
|
||||
apiKeyDataController,
|
||||
secretController,
|
||||
secretsController
|
||||
secretsController,
|
||||
environmentController
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
import { pushKeys } from '../../helpers/key';
|
||||
import { postHogClient, EventService } from '../../services';
|
||||
import { eventPushSecrets } from '../../events';
|
||||
import { ENV_SET } from '../../variables';
|
||||
|
||||
interface V2PushSecret {
|
||||
type: string; // personal or shared
|
||||
@ -52,7 +51,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
@ -129,6 +129,11 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
||||
} else if (req.serviceTokenData) {
|
||||
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({
|
||||
userId,
|
||||
|
@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../../variables';
|
||||
|
||||
export interface ISecretVersion {
|
||||
@ -56,7 +52,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
isDeleted: { // consider removing field
|
||||
|
@ -7,8 +7,6 @@ import {
|
||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||
import { BotService } from '../services';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
@ -36,11 +34,13 @@ interface Update {
|
||||
const handleOAuthExchangeHelper = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
let action;
|
||||
let integrationAuth;
|
||||
@ -102,9 +102,9 @@ const handleOAuthExchangeHelper = async ({
|
||||
// initialize new integration after exchange
|
||||
await new Integration({
|
||||
workspace: workspaceId,
|
||||
environment: ENV_DEV,
|
||||
isActive: false,
|
||||
app: null,
|
||||
environment,
|
||||
integration,
|
||||
integrationAuth: integrationAuth._id
|
||||
}).save();
|
||||
|
@ -1,9 +1,5 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
@ -13,7 +9,7 @@ import {
|
||||
export interface IIntegration {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||
environment: string;
|
||||
isActive: boolean;
|
||||
app: string;
|
||||
target: string;
|
||||
@ -32,7 +28,6 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
|
@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD
|
||||
} from '../variables';
|
||||
|
||||
export interface ISecret {
|
||||
@ -53,7 +49,6 @@ const secretSchema = new Schema<ISecret>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import { ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD } from '../variables';
|
||||
|
||||
// TODO: deprecate
|
||||
export interface IServiceToken {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
@ -33,7 +30,6 @@ const serviceTokenSchema = new Schema<IServiceToken>(
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
|
@ -4,6 +4,10 @@ export interface IWorkspace {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
organization: Types.ObjectId;
|
||||
environments: Array<{
|
||||
name: string;
|
||||
slug: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const workspaceSchema = new Schema<IWorkspace>({
|
||||
@ -15,7 +19,33 @@ const workspaceSchema = new Schema<IWorkspace>({
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Organization',
|
||||
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);
|
||||
|
57
backend/src/routes/v2/environment.ts
Normal file
57
backend/src/routes/v2/environment.ts
Normal 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;
|
@ -3,11 +3,13 @@ import secrets from './secrets';
|
||||
import workspace from './workspace';
|
||||
import serviceTokenData from './serviceTokenData';
|
||||
import apiKeyData from './apiKeyData';
|
||||
import environment from "./environment"
|
||||
|
||||
export {
|
||||
secret,
|
||||
secrets,
|
||||
workspace,
|
||||
serviceTokenData,
|
||||
apiKeyData
|
||||
}
|
||||
apiKeyData,
|
||||
environment
|
||||
}
|
@ -18,7 +18,7 @@ import {
|
||||
router.post(
|
||||
'/',
|
||||
body('workspaceId').exists().isString().trim(),
|
||||
body('environment').exists().isString().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
||||
body('environment').exists().isString().trim(),
|
||||
body('secrets')
|
||||
.exists()
|
||||
.custom((value) => {
|
||||
@ -73,7 +73,7 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
query('workspaceId').exists().trim(),
|
||||
query('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
||||
query('environment').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||
|
@ -11,10 +11,6 @@ import {
|
||||
setIntegrationAuthAccessHelper,
|
||||
} from '../helpers/integration';
|
||||
import { exchangeCode } from '../integrations';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
// should sync stuff be here too? Probably.
|
||||
// TODO: move bot functions to IntegrationService.
|
||||
@ -32,22 +28,26 @@ class IntegrationService {
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - workspace environment
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
*/
|
||||
static async handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
environment: string;
|
||||
}) {
|
||||
await handleOAuthExchangeHelper({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
code,
|
||||
environment
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ export default function ListBox({
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
{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}
|
||||
</span>
|
||||
@ -69,7 +69,7 @@ export default function ListBox({
|
||||
<Listbox.Option
|
||||
key={personIdx}
|
||||
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' : ''
|
||||
} ${
|
||||
active && !selected
|
||||
|
@ -8,7 +8,6 @@ import nacl from "tweetnacl";
|
||||
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import { envMapping } from "../../../public/data/frequentConstants";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
serviceTokens,
|
||||
environments,
|
||||
setServiceTokens
|
||||
}) => {
|
||||
const [serviceToken, setServiceToken] = useState("");
|
||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
|
||||
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@ -66,7 +66,7 @@ const AddServiceTokenDialog = ({
|
||||
let newServiceToken = await addServiceToken({
|
||||
name: serviceTokenName,
|
||||
workspaceId,
|
||||
environment: envMapping[serviceTokenEnv],
|
||||
environment: selectedServiceTokenEnv?.slug ? selectedServiceTokenEnv.slug : environments[0]?.name,
|
||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
@ -101,155 +101,159 @@ const AddServiceTokenDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-50">
|
||||
<div className='z-50'>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
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>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<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"
|
||||
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'
|
||||
>
|
||||
{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">
|
||||
{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.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
as='h3'
|
||||
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,
|
||||
})}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("section-token:add-dialog.description")}
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-token:add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-28 mb-2">
|
||||
<div className='max-h-28 mb-2'>
|
||||
<InputField
|
||||
label={t("section-token:add-dialog.name")}
|
||||
label={t('section-token:add-dialog.name')}
|
||||
onChangeHandler={setServiceTokenName}
|
||||
type="varName"
|
||||
type='varName'
|
||||
value={serviceTokenName}
|
||||
placeholder=""
|
||||
placeholder=''
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-28 mb-2">
|
||||
<div className='max-h-28 mb-2'>
|
||||
<ListBox
|
||||
selected={serviceTokenEnv}
|
||||
onChange={setServiceTokenEnv}
|
||||
data={[
|
||||
"Development",
|
||||
"Staging",
|
||||
"Production",
|
||||
"Testing",
|
||||
]}
|
||||
selected={selectedServiceTokenEnv?.name ? selectedServiceTokenEnv?.name : environments[0]?.name}
|
||||
data={environments.map(({ name }) => name)}
|
||||
onChange={(envName) =>
|
||||
setSelectedServiceTokenEnv(
|
||||
environments.find(
|
||||
({ name }) => envName === name
|
||||
) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull={true}
|
||||
text={`${t("common:environment")}: `}
|
||||
text={`${t('common:environment')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
<div className='max-h-28'>
|
||||
<ListBox
|
||||
selected={serviceTokenExpiresIn}
|
||||
onChange={setServiceTokenExpiresIn}
|
||||
data={[
|
||||
"1 day",
|
||||
"7 days",
|
||||
"1 month",
|
||||
"6 months",
|
||||
"12 months",
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
isFull={true}
|
||||
text={`${t("common:expired-in")}: `}
|
||||
text={`${t('common:expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-max">
|
||||
<div className="mt-6 flex flex-col justify-start w-max">
|
||||
<div className='max-w-max'>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={() => generateServiceToken()}
|
||||
color="mineshaft"
|
||||
text={t("section-token:add-dialog.add")}
|
||||
textDisabled={t("section-token:add-dialog.add")}
|
||||
size="md"
|
||||
active={serviceTokenName == "" ? false : true}
|
||||
color='mineshaft'
|
||||
text={t('section-token:add-dialog.add')}
|
||||
textDisabled={t('section-token:add-dialog.add')}
|
||||
size='md'
|
||||
active={serviceTokenName == '' ? false : true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
as='h3'
|
||||
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>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t(
|
||||
"section-token:add-dialog.copy-service-token-description"
|
||||
'section-token:add-dialog.copy-service-token-description'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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='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'>
|
||||
<input
|
||||
type="text"
|
||||
type='text'
|
||||
value={serviceToken}
|
||||
id="serviceToken"
|
||||
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
|
||||
id='serviceToken'
|
||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||
></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}
|
||||
</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
|
||||
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 ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className="pr-0.5"
|
||||
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 px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
|
||||
{t("common:click-to-copy")}
|
||||
<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')}
|
||||
</span>
|
||||
</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
|
||||
onButtonPressed={() => closeAddServiceTokenModal()}
|
||||
color="mineshaft"
|
||||
text="Close"
|
||||
size="md"
|
||||
color='mineshaft'
|
||||
text='Close'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
145
frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx
Normal file
145
frontend/components/basic/dialog/AddUpdateEnvironmentDialog.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
104
frontend/components/basic/dialog/DeleteActionModal.tsx
Normal file
104
frontend/components/basic/dialog/DeleteActionModal.tsx
Normal file
@ -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;
|
167
frontend/components/basic/table/EnvironmentsTable.tsx
Normal file
167
frontend/components/basic/table/EnvironmentsTable.tsx
Normal file
@ -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 deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
||||
{workspaceName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{reverseEnvMapping[row.environment]}
|
||||
{row.environment}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{new Date(row.expiresAt).toUTCString()}
|
||||
|
@ -74,7 +74,7 @@ const CloudIntegration = ({
|
||||
integrationAuths
|
||||
.map((authorization) => authorization.integration)
|
||||
.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
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
@ -15,9 +15,7 @@ import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"
|
||||
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
||||
import {
|
||||
contextNetlifyMapping,
|
||||
envMapping,
|
||||
reverseContextNetlifyMapping,
|
||||
reverseEnvMapping,
|
||||
} from "../../public/data/frequentConstants";
|
||||
|
||||
interface Integration {
|
||||
@ -36,13 +34,23 @@ interface IntegrationApp {
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
const Integration = ({
|
||||
integration
|
||||
}: {
|
||||
type Props = {
|
||||
integration: Integration;
|
||||
}) => {
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState(
|
||||
reverseEnvMapping[integration.environment]
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
};
|
||||
|
||||
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 router = useRouter();
|
||||
@ -93,7 +101,7 @@ const Integration = ({
|
||||
case "vercel":
|
||||
return (
|
||||
<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
|
||||
</div>
|
||||
<ListBox
|
||||
@ -104,6 +112,7 @@ const Integration = ({
|
||||
] : null}
|
||||
selected={integrationTarget}
|
||||
onChange={setIntegrationTarget}
|
||||
isFull={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -136,42 +145,47 @@ const Integration = ({
|
||||
if (!integrationApp || apps.length === 0) return <div></div>
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||
<div className="flex">
|
||||
<div className='max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between'>
|
||||
<div className='flex'>
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
||||
<ListBox data={!integration.isActive ? [
|
||||
"Development",
|
||||
"Staging",
|
||||
"Testing",
|
||||
"Production",
|
||||
] : null}
|
||||
selected={integrationEnvironment}
|
||||
onChange={(environment) => {
|
||||
setIntegrationEnvironment(environment);
|
||||
}}
|
||||
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
||||
ENVIRONMENT
|
||||
</p>
|
||||
<ListBox
|
||||
data={
|
||||
!integration.isActive
|
||||
? environments.map(({ name }) => name)
|
||||
: null
|
||||
}
|
||||
selected={integrationEnvironment.name}
|
||||
onChange={(envName) =>
|
||||
setIntegrationEnvironment(
|
||||
environments.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<div className='pt-2'>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="mx-4 text-gray-400 mt-8"
|
||||
/>
|
||||
className='mx-4 text-gray-400 mt-8'
|
||||
/>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">
|
||||
<div className='mr-2'>
|
||||
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
||||
INTEGRATION
|
||||
</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.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||
APP
|
||||
</div>
|
||||
<div className='mr-2'>
|
||||
<div className='text-gray-400 text-xs font-semibold mb-2'>APP</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
selected={integrationApp}
|
||||
@ -182,52 +196,55 @@ const Integration = ({
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
{integration.isActive ? (
|
||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||
<FontAwesomeIcon
|
||||
icon={faRotate}
|
||||
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='flex items-end'>
|
||||
{integration.isActive ? (
|
||||
<div className='max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4'>
|
||||
<FontAwesomeIcon
|
||||
icon={faRotate}
|
||||
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;
|
||||
|
||||
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>
|
||||
);
|
||||
|
@ -5,7 +5,8 @@ import guidGenerator from "~/utilities/randomId";
|
||||
import Integration from "./Integration";
|
||||
|
||||
interface Props {
|
||||
integrations: any
|
||||
integrations: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
}
|
||||
|
||||
interface IntegrationType {
|
||||
@ -19,7 +20,8 @@ interface IntegrationType {
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({
|
||||
integrations
|
||||
integrations,
|
||||
environments = [],
|
||||
}: Props) => {
|
||||
return integrations.length > 0 ? (
|
||||
<div className="mb-12">
|
||||
@ -33,6 +35,7 @@ const ProjectIntegrationSection = ({
|
||||
<Integration
|
||||
key={guidGenerator()}
|
||||
integration={integration}
|
||||
environments={environments}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = envMapping[env] + '.env';
|
||||
alink.download = env + '.env';
|
||||
alink.click();
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
import getSecrets from '~/pages/api/files/GetSecrets';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
import { envMapping } from '../../../public/data/frequentConstants';
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
@ -35,7 +33,7 @@ interface SecretProps {
|
||||
}
|
||||
|
||||
interface FunctionProps {
|
||||
env: keyof typeof envMapping;
|
||||
env: string;
|
||||
setIsKeyAvailable: any;
|
||||
setData: any;
|
||||
workspaceId: string;
|
||||
@ -58,7 +56,7 @@ const getSecretsForProject = async ({
|
||||
try {
|
||||
let encryptedSecrets;
|
||||
try {
|
||||
encryptedSecrets = await getSecrets(workspaceId, envMapping[env]);
|
||||
encryptedSecrets = await getSecrets(workspaceId, env);
|
||||
} catch (error) {
|
||||
console.log('ERROR: Not able to access the latest version of secrets');
|
||||
}
|
||||
|
1
frontend/hooks/index.ts
Normal file
1
frontend/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { usePopUp } from './usePopUp';
|
69
frontend/hooks/usePopUp.tsx
Normal file
69
frontend/hooks/usePopUp.tsx
Normal 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,
|
||||
};
|
||||
};
|
29
frontend/pages/api/environments/createEnvironment.ts
Normal file
29
frontend/pages/api/environments/createEnvironment.ts
Normal file
@ -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;
|
26
frontend/pages/api/environments/deleteEnvironment.ts
Normal file
26
frontend/pages/api/environments/deleteEnvironment.ts
Normal file
@ -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;
|
33
frontend/pages/api/environments/updateEnvironment.ts
Normal file
33
frontend/pages/api/environments/updateEnvironment.ts
Normal file
@ -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;
|
31
frontend/pages/api/workspace/getAWorkspace.ts
Normal file
31
frontend/pages/api/workspace/getAWorkspace.ts
Normal file
@ -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;
|
||||
name: string;
|
||||
organization: string;
|
||||
environments: Array<{name:string, slug:string}>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
faArrowDownAZ,
|
||||
faArrowDownZA,
|
||||
@ -34,7 +34,6 @@ import getSecretsForProject from '~/components/utilities/secrets/getSecretsForPr
|
||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||
import guidGenerator from '~/utilities/randomId';
|
||||
|
||||
import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants';
|
||||
import addSecrets from '../api/files/AddSecrets';
|
||||
import deleteSecrets from '../api/files/DeleteSecrets';
|
||||
import updateSecrets from '../api/files/UpdateSecrets';
|
||||
@ -43,6 +42,10 @@ import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
|
||||
type WorkspaceEnv = {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
interface SecretDataProps {
|
||||
pos: number;
|
||||
@ -104,11 +107,8 @@ export default function Dashboard() {
|
||||
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const [workspaceId, setWorkspaceId] = useState('');
|
||||
const [blurred, setBlurred] = useState(true);
|
||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||
const [env, setEnv] = useState('Development');
|
||||
const [snapshotEnv, setSnapshotEnv] = useState('Development');
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchKeys, setSearchKeys] = useState('');
|
||||
@ -116,7 +116,7 @@ export default function Dashboard() {
|
||||
const [sortMethod, setSortMethod] = useState('alphabetical');
|
||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||
const [sidebarSecretId, toggleSidebar] = useState("None");
|
||||
const [sidebarSecretId, toggleSidebar] = useState('None');
|
||||
const [PITSidebarOpen, togglePITSidebar] = useState(false);
|
||||
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
||||
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
|
||||
@ -126,6 +126,16 @@ export default function Dashboard() {
|
||||
const { t } = useTranslation();
|
||||
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
|
||||
// const beforeRouteHandler = (url) => {
|
||||
// const warningText =
|
||||
@ -172,25 +182,37 @@ export default function Dashboard() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) })
|
||||
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
|
||||
workspaceId,
|
||||
});
|
||||
setNumSnapshots(tempNumSnapshots);
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
||||
if (
|
||||
!listWorkspaces.includes(router.asPath.split('/')[2])
|
||||
) {
|
||||
router.push('/dashboard/' + listWorkspaces[0]);
|
||||
const workspace = userWorkspaces.find(
|
||||
(workspace) => workspace._id === workspaceId
|
||||
);
|
||||
if (!workspace) {
|
||||
router.push('/dashboard/' + userWorkspaces?.[0]?._id);
|
||||
}
|
||||
|
||||
setWorkspaceEnvs(workspace?.environments || []);
|
||||
// set env
|
||||
const env = workspace?.environments?.[0] || {
|
||||
name: 'unknown',
|
||||
slug: 'unkown',
|
||||
};
|
||||
setSelectedEnv(env);
|
||||
setSelectedSnapshotEnv(env);
|
||||
const user = await getUser();
|
||||
setIsNew(
|
||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) /
|
||||
60000 <
|
||||
3
|
||||
? true
|
||||
: false
|
||||
);
|
||||
|
||||
const userAction = await checkUserAction({
|
||||
action: 'first_time_secrets_pushed'
|
||||
action: 'first_time_secrets_pushed',
|
||||
});
|
||||
setHasUserEverPushed(userAction ? true : false);
|
||||
} catch (error) {
|
||||
@ -205,25 +227,26 @@ export default function Dashboard() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setBlurred(true);
|
||||
setWorkspaceId(String(router.query.id));
|
||||
|
||||
// ENV
|
||||
const dataToSort = await getSecretsForProject({
|
||||
env,
|
||||
env: selectedEnv.slug,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId: String(router.query.id)
|
||||
workspaceId,
|
||||
});
|
||||
setInitialData(dataToSort);
|
||||
reorderRows(dataToSort);
|
||||
|
||||
setIsLoading(false);
|
||||
setTimeout(
|
||||
() => setIsLoading(false)
|
||||
, 700);
|
||||
} catch (error) {
|
||||
console.log('Error', error);
|
||||
setData(undefined);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [env]);
|
||||
}, [selectedEnv]);
|
||||
|
||||
const addRow = () => {
|
||||
setIsNew(false);
|
||||
@ -237,37 +260,22 @@ export default function Dashboard() {
|
||||
value: '',
|
||||
valueOverride: undefined,
|
||||
comment: '',
|
||||
}
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
||||
setButtonReady(true);
|
||||
toggleSidebar("None");
|
||||
toggleSidebar('None');
|
||||
createNotification({
|
||||
text: `${secretName} has been deleted. Remember to save changes.`,
|
||||
type: 'error'
|
||||
type: 'error',
|
||||
});
|
||||
sortValuesHandler(data!.filter((row: SecretDataProps) => !ids.includes(row.id)), sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
||||
};
|
||||
|
||||
/**
|
||||
* 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")
|
||||
sortValuesHandler(
|
||||
data!.filter((row: SecretDataProps) => !ids.includes(row.id)),
|
||||
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
|
||||
);
|
||||
};
|
||||
|
||||
const modifyValue = (value: string, pos: number) => {
|
||||
@ -341,14 +349,14 @@ export default function Dashboard() {
|
||||
if (nameErrors) {
|
||||
return createNotification({
|
||||
text: 'Solve all name errors before saving secrets.',
|
||||
type: 'error'
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
if (duplicatesExist) {
|
||||
return createNotification({
|
||||
text: 'Remove duplicated secret names before saving.',
|
||||
type: 'error'
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
@ -404,11 +412,11 @@ export default function Dashboard() {
|
||||
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
|
||||
}
|
||||
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
|
||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: envMapping[env] });
|
||||
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId });
|
||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: selectedEnv.slug });
|
||||
secrets && await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
|
||||
@ -434,36 +442,49 @@ export default function Dashboard() {
|
||||
setBlurred(!blurred);
|
||||
};
|
||||
|
||||
const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => {
|
||||
const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
||||
const sortValuesHandler = (
|
||||
dataToSort: SecretDataProps[] | 1,
|
||||
specificSortMethod?: 'alphabetical' | '-alphabetical'
|
||||
) => {
|
||||
const howToSort =
|
||||
specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
||||
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
||||
.sort((a, b) =>
|
||||
howToSort == 'alphabetical'
|
||||
? a.key.localeCompare(b.key)
|
||||
: b.key.localeCompare(a.key)
|
||||
)
|
||||
.map((item: SecretDataProps, index: number) => {
|
||||
return {
|
||||
...item,
|
||||
pos: index
|
||||
};
|
||||
});
|
||||
.sort((a, b) =>
|
||||
howToSort == 'alphabetical'
|
||||
? a.key.localeCompare(b.key)
|
||||
: b.key.localeCompare(a.key)
|
||||
)
|
||||
.map((item: SecretDataProps, index: number) => {
|
||||
return {
|
||||
...item,
|
||||
pos: index,
|
||||
};
|
||||
});
|
||||
|
||||
setData(sortedData);
|
||||
};
|
||||
|
||||
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
||||
deleteRow({ids, secretName});
|
||||
|
||||
const deleteCertainRow = ({
|
||||
ids,
|
||||
secretName,
|
||||
}: {
|
||||
ids: string[];
|
||||
secretName: string;
|
||||
}) => {
|
||||
deleteRow({ ids, secretName });
|
||||
};
|
||||
|
||||
return data ? (
|
||||
<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>
|
||||
<title>{t("common:head-title", { title: t("dashboard:title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={String(t("dashboard:og-title"))} />
|
||||
<meta name="og:description" content={String(t("dashboard:og-description"))} />
|
||||
<title>{t('common:head-title', { title: t('dashboard:title') })}</title>
|
||||
<link rel='icon' href='/infisical.ico' />
|
||||
<meta property='og:image' content='/images/message.png' />
|
||||
<meta property='og:title' content={String(t('dashboard:og-title'))} />
|
||||
<meta
|
||||
name='og:description'
|
||||
content={String(t('dashboard:og-description'))}
|
||||
/>
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
{sidebarSecretId != "None" && <SideBar
|
||||
@ -488,56 +509,68 @@ export default function Dashboard() {
|
||||
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
|
||||
{checkDocsPopUpVisible && (
|
||||
<BottonRightPopup
|
||||
buttonText={t("dashboard:check-docs.button")}
|
||||
buttonLink="https://infisical.com/docs/getting-started/introduction"
|
||||
titleText={t("dashboard:check-docs.title")}
|
||||
emoji="🎉"
|
||||
textLine1={t("dashboard:check-docs.line1")}
|
||||
textLine2={t("dashboard:check-docs.line2")}
|
||||
buttonText={t('dashboard:check-docs.button')}
|
||||
buttonLink='https://infisical.com/docs/getting-started/introduction'
|
||||
titleText={t('dashboard:check-docs.title')}
|
||||
emoji='🎉'
|
||||
textLine1={t('dashboard:check-docs.line1')}
|
||||
textLine2={t('dashboard:check-docs.line2')}
|
||||
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
|
||||
{snapshotData &&
|
||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||
<Button
|
||||
text={String(t("Go back to current"))}
|
||||
onButtonPressed={() => setSnapshotData(undefined)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faArrowLeft}
|
||||
/>
|
||||
</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">
|
||||
<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 className='flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl'>
|
||||
{snapshotData && (
|
||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||
<Button
|
||||
text={String(t('Go back to current'))}
|
||||
onButtonPressed={() => setSnapshotData(undefined)}
|
||||
color='mineshaft'
|
||||
size='md'
|
||||
icon={faArrowLeft}
|
||||
/>
|
||||
</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'>
|
||||
<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>
|
||||
{!snapshotData && data?.length == 0 && (
|
||||
<ListBox
|
||||
selected={env}
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
onChange={setEnv}
|
||||
selected={selectedEnv.name}
|
||||
data={workspaceEnvs.map(({ name }) => name)}
|
||||
onChange={(envName) =>
|
||||
setSelectedEnv(
|
||||
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className='flex flex-row'>
|
||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||
<Button
|
||||
text={String(numSnapshots + " " + t("Commits"))}
|
||||
text={String(numSnapshots + ' ' + t('Commits'))}
|
||||
onButtonPressed={() => togglePITSidebar(true)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
color='mineshaft'
|
||||
size='md'
|
||||
icon={faClockRotateLeft}
|
||||
/>
|
||||
</div>
|
||||
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
||||
<div className={`flex justify-start max-w-sm mt-1`}>
|
||||
<Button
|
||||
text={String(t("common:save-changes"))}
|
||||
text={String(t('common:save-changes'))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
color='primary'
|
||||
size='md'
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled={String(t("common:saved"))}
|
||||
@ -551,7 +584,7 @@ export default function Dashboard() {
|
||||
onButtonPressed={async () => {
|
||||
// Update secrets in the state only for the current environment
|
||||
const rolledBackSecrets = snapshotData.secretVersions
|
||||
.filter(row => reverseEnvMapping[row.environment] == env)
|
||||
.filter(row => row.environment == selectedEnv.slug)
|
||||
.map((sv, position) => {
|
||||
return {
|
||||
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 className="mx-6 w-full pr-12">
|
||||
<div className="flex flex-col max-w-5xl pb-1">
|
||||
<div className="w-full flex flex-row items-start">
|
||||
<div className='mx-6 w-full pr-12'>
|
||||
<div className='flex flex-col max-w-5xl pb-1'>
|
||||
<div className='w-full flex flex-row items-start'>
|
||||
{(snapshotData || data?.length !== 0) && (
|
||||
<>
|
||||
{!snapshotData
|
||||
? <ListBox
|
||||
selected={env}
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
onChange={setEnv}
|
||||
/>
|
||||
: <ListBox
|
||||
selected={snapshotEnv}
|
||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
||||
onChange={setSnapshotEnv}
|
||||
/>}
|
||||
<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">
|
||||
{!snapshotData ? (
|
||||
<ListBox
|
||||
selected={selectedEnv.name}
|
||||
data={workspaceEnvs.map(({ name }) => name)}
|
||||
onChange={(envName) =>
|
||||
setSelectedEnv(
|
||||
workspaceEnvs.find(
|
||||
({ name }) => envName === name
|
||||
) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
}
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<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
|
||||
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}
|
||||
/>
|
||||
<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}
|
||||
onChange={(e) => setSearchKeys(e.target.value)}
|
||||
placeholder={String(t("dashboard:search-keys"))}
|
||||
placeholder={String(t('dashboard:search-keys'))}
|
||||
/>
|
||||
</div>
|
||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
onButtonPressed={() => reorderRows(1)}
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={
|
||||
sortMethod == 'alphabetical'
|
||||
? faArrowDownAZ
|
||||
: faArrowDownZA
|
||||
}
|
||||
/>
|
||||
</div>}
|
||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<DownloadSecretMenu data={data} env={env} />
|
||||
</div>}
|
||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
{!snapshotData && (
|
||||
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
|
||||
<Button
|
||||
onButtonPressed={() => reorderRows(1)}
|
||||
color='mineshaft'
|
||||
size='icon-md'
|
||||
icon={
|
||||
sortMethod == 'alphabetical'
|
||||
? faArrowDownAZ
|
||||
: faArrowDownZA
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!snapshotData && (
|
||||
<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
|
||||
onButtonPressed={changeBlurred}
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
color='mineshaft'
|
||||
size='icon-md'
|
||||
icon={blurred ? faEye : faEyeSlash}
|
||||
/>
|
||||
</div>
|
||||
{!snapshotData && <div className="relative ml-2 min-w-max flex flex-row items-start justify-end">
|
||||
<Button
|
||||
text={String(t("dashboard:add-key"))}
|
||||
onButtonPressed={addRow}
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
{isNew && (
|
||||
<span className="absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4">
|
||||
<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="relative inline-flex rounded-full h-3 w-3 bg-primary"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>}
|
||||
{!snapshotData && (
|
||||
<div className='relative ml-2 min-w-max flex flex-row items-start justify-end'>
|
||||
<Button
|
||||
text={String(t('dashboard:add-key'))}
|
||||
onButtonPressed={addRow}
|
||||
color='mineshaft'
|
||||
icon={faPlus}
|
||||
size='md'
|
||||
/>
|
||||
{isNew && (
|
||||
<span className='absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4'>
|
||||
<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='relative inline-flex rounded-full h-3 w-3 bg-primary'></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full my-48">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
data?.length !== 0 ? (
|
||||
<div className="flex flex-col w-full mt-1 mb-2">
|
||||
<div className='flex items-center justify-center h-full my-48'>
|
||||
<Image
|
||||
src='/images/loading/loading.gif'
|
||||
height={60}
|
||||
width={100}
|
||||
alt='infisical loading indicator'
|
||||
></Image>
|
||||
</div>
|
||||
) : data?.length !== 0 ? (
|
||||
<div className='flex flex-col w-full mt-1 mb-2'>
|
||||
<div
|
||||
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))
|
||||
.filter(row => reverseEnvMapping[row.environment] == snapshotEnv)
|
||||
.filter(row => row.environment == selectedSnapshotEnv?.slug)
|
||||
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||
.filter(
|
||||
row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
|
||||
@ -707,21 +768,23 @@ export default function Dashboard() {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!snapshotData && <div className="w-full max-w-5xl px-2 pt-3">
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</div>}
|
||||
{!snapshotData && (
|
||||
<div className='w-full max-w-5xl px-2 pt-3'>
|
||||
<DropZone
|
||||
setData={addData}
|
||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||
createNewFile={addRow}
|
||||
errorDragAndDrop={errorDragAndDrop}
|
||||
setButtonReady={setButtonReady}
|
||||
keysExist={true}
|
||||
numCurrentRows={data.length}
|
||||
/>
|
||||
</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 && (
|
||||
<DropZone
|
||||
setData={setData}
|
||||
@ -733,36 +796,35 @@ export default function Dashboard() {
|
||||
keysExist={false}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
(!isKeyAvailable && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
className="text-7xl mt-20 mb-8"
|
||||
icon={faFolderOpen}
|
||||
/>
|
||||
<p>
|
||||
To view this file, contact your administrator for
|
||||
permission.
|
||||
</p>
|
||||
<p className="mt-1">
|
||||
They need to grant you access in the team tab.
|
||||
</p>
|
||||
</>
|
||||
))}
|
||||
{!isKeyAvailable && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
className='text-7xl mt-20 mb-8'
|
||||
icon={faFolderOpen}
|
||||
/>
|
||||
<p>
|
||||
To view this file, contact your administrator for
|
||||
permission.
|
||||
</p>
|
||||
<p className='mt-1'>
|
||||
They need to grant you access in the team tab.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</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="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></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='absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full'></div>
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
src='/images/loading/loading.gif'
|
||||
height={70}
|
||||
width={120}
|
||||
alt="loading animation"
|
||||
alt='loading animation'
|
||||
></Image>
|
||||
</div>
|
||||
);
|
||||
@ -770,4 +832,4 @@ export default function Dashboard() {
|
||||
|
||||
Dashboard.requireAuth = true;
|
||||
|
||||
export const getServerSideProps = getTranslatedServerSideProps(["dashboard"]);
|
||||
export const getServerSideProps = getTranslatedServerSideProps(['dashboard']);
|
||||
|
@ -24,6 +24,7 @@ import setBotActiveStatus from "../api/bot/setBotActiveStatus";
|
||||
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
|
||||
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
||||
import getAWorkspace from "../api/workspace/getAWorkspace";
|
||||
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
@ -34,6 +35,7 @@ const crypto = require("crypto");
|
||||
export default function Integrations() {
|
||||
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
|
||||
const [integrationAuths, setIntegrationAuths] = useState([]);
|
||||
const [environments,setEnvironments] = useState([])
|
||||
const [integrations, setIntegrations] = useState([]);
|
||||
const [bot, setBot] = useState(null);
|
||||
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
|
||||
@ -41,11 +43,15 @@ export default function Integrations() {
|
||||
const [selectedIntegrationOption, setSelectedIntegrationOption] = useState(null);
|
||||
|
||||
const router = useRouter();
|
||||
const workspaceId = router.query.id;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(async () => {
|
||||
try {
|
||||
const workspace = await getAWorkspace(workspaceId);
|
||||
setEnvironments(workspace.environments);
|
||||
|
||||
// get cloud integration options
|
||||
setCloudIntegrationOptions(
|
||||
await getIntegrationOptions()
|
||||
@ -54,23 +60,19 @@ export default function Integrations() {
|
||||
// get project integration authorizations
|
||||
setIntegrationAuths(
|
||||
await getWorkspaceAuthorizations({
|
||||
workspaceId: router.query.id,
|
||||
workspaceId
|
||||
})
|
||||
);
|
||||
|
||||
// get project integrations
|
||||
setIntegrations(
|
||||
await getWorkspaceIntegrations({
|
||||
workspaceId: router.query.id,
|
||||
workspaceId,
|
||||
})
|
||||
);
|
||||
|
||||
// get project bot
|
||||
setBot(
|
||||
await getBot({
|
||||
workspaceId: router.query.id
|
||||
}
|
||||
));
|
||||
setBot(await getBot({ workspaceId }));
|
||||
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
@ -90,7 +92,7 @@ export default function Integrations() {
|
||||
|
||||
if (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 WORKSPACE_KEY = decryptAssymmetric({
|
||||
@ -214,7 +216,7 @@ export default function Integrations() {
|
||||
handleBotActivate={handleBotActivate}
|
||||
handleIntegrationOption={handleIntegrationOption}
|
||||
/> */}
|
||||
<IntegrationSection integrations={integrations} />
|
||||
<IntegrationSection integrations={integrations} environments={environments} />
|
||||
{(cloudIntegrationOptions.length > 0 && bot) ? (
|
||||
<CloudIntegrationSection
|
||||
cloudIntegrationOptions={cloudIntegrationOptions}
|
||||
|
@ -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
frontend/pages/settings/project/[id].tsx
Normal file
358
frontend/pages/settings/project/[id].tsx
Normal 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',
|
||||
]);
|
@ -3,6 +3,7 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/components/*": ["components/*"],
|
||||
"~/hooks/*": ["hooks/*"],
|
||||
"~/utilities/*": ["components/utilities/*"],
|
||||
"~/*": ["const"],
|
||||
"~/pages/*": ["pages/*"]
|
||||
|
Reference in New Issue
Block a user