mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 08:28:52 +00:00
feat(#31): implemented ui for multi env and integrated api with backend
fix(#31): fixed all v2 release conflict
This commit is contained in:
+1
-1
@@ -104,12 +104,12 @@ app.use('/api/v1/integration', v1IntegrationRouter);
|
|||||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
|
|
||||||
// v2 routes
|
// v2 routes
|
||||||
|
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||||
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
||||||
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
||||||
app.use('/api/v2/secrets', v2SecretsRouter);
|
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
||||||
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
||||||
app.use('/api/v2/environments', v2EnvironmentRouter);
|
|
||||||
|
|
||||||
// api docs
|
// api docs
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { IntegrationAuth, Integration } from '../../models';
|
import { IntegrationAuth, Integration } from '../../models';
|
||||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
|
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||||
import { IntegrationService } from '../../services';
|
import { IntegrationService } from '../../services';
|
||||||
import { getApps, revokeAccess } from '../../integrations';
|
import { getApps, revokeAccess } from '../../integrations';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Secret, ServiceToken, Workspace, Integration } from '../../models';
|
import {
|
||||||
|
Secret,
|
||||||
|
ServiceToken,
|
||||||
|
Workspace,
|
||||||
|
Integration,
|
||||||
|
ServiceTokenData,
|
||||||
|
} from '../../models';
|
||||||
|
import { SecretVersion } from '../../ee/models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new workspace environment named [environmentName] under workspace with id
|
* Create new workspace environment named [environmentName] under workspace with id
|
||||||
@@ -12,25 +19,24 @@ export const createWorkspaceEnvironment = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
const { workspaceId, environmentName, environmentSlug } = req.body;
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentName, environmentSlug } = req.body;
|
||||||
try {
|
try {
|
||||||
// atomic create the environment
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
const workspace = await Workspace.findOneAndUpdate(
|
if (
|
||||||
{
|
!workspace ||
|
||||||
_id: workspaceId,
|
workspace?.environments.find(
|
||||||
'environments.slug': { $ne: environmentSlug },
|
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||||
'environments.name': { $ne: environmentName },
|
)
|
||||||
},
|
) {
|
||||||
{
|
throw new Error('Failed to create workspace environment');
|
||||||
$addToSet: {
|
|
||||||
environments: { name: environmentName, slug: environmentSlug },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
if (!workspace) {
|
workspace?.environments.push({
|
||||||
throw new Error('Failed to update workspace environment');
|
name: environmentName.toLowerCase(),
|
||||||
}
|
slug: environmentSlug.toLowerCase(),
|
||||||
|
});
|
||||||
|
await workspace.save();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -60,8 +66,8 @@ export const renameWorkspaceEnvironment = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
const { workspaceId, environmentName, environmentSlug, oldEnvironmentSlug } =
|
const { workspaceId } = req.params;
|
||||||
req.body;
|
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
||||||
try {
|
try {
|
||||||
// user should pass both new slug and env name
|
// user should pass both new slug and env name
|
||||||
if (!environmentSlug || !environmentName) {
|
if (!environmentSlug || !environmentName) {
|
||||||
@@ -69,25 +75,47 @@ export const renameWorkspaceEnvironment = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// atomic update the env to avoid conflict
|
// atomic update the env to avoid conflict
|
||||||
const workspace = await Workspace.findOneAndUpdate(
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
{ _id: workspaceId, 'environments.slug': oldEnvironmentSlug },
|
|
||||||
{
|
|
||||||
'environments.$.name': environmentName,
|
|
||||||
'environments.$.slug': environmentSlug,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new Error('Failed to update workspace');
|
throw new Error('Failed to create workspace environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isEnvExist = workspace.environments.some(
|
||||||
|
({ name, slug }) =>
|
||||||
|
slug !== oldEnvironmentSlug &&
|
||||||
|
(name === environmentName || slug === environmentSlug)
|
||||||
|
);
|
||||||
|
if (isEnvExist) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === oldEnvironmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments[envIndex].name = environmentName.toLowerCase();
|
||||||
|
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||||
|
|
||||||
|
await workspace.save();
|
||||||
await Secret.updateMany(
|
await Secret.updateMany(
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
{ environment: environmentSlug }
|
{ environment: environmentSlug }
|
||||||
);
|
);
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
await ServiceToken.updateMany(
|
await ServiceToken.updateMany(
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
{ environment: environmentSlug }
|
{ environment: environmentSlug }
|
||||||
);
|
);
|
||||||
|
await ServiceTokenData.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
await Integration.updateMany(
|
await Integration.updateMany(
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
{ environment: environmentSlug }
|
{ environment: environmentSlug }
|
||||||
@@ -120,37 +148,46 @@ export const deleteWorkspaceEnvironment = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
const { workspaceId, environmentSlug } = req.body;
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentSlug } = req.body;
|
||||||
try {
|
try {
|
||||||
// atomic delete the env in the workspacce
|
// atomic update the env to avoid conflict
|
||||||
const workspace = await Workspace.findOneAndUpdate(
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
{ _id: workspaceId },
|
|
||||||
{
|
|
||||||
$pull: {
|
|
||||||
environments: {
|
|
||||||
slug: environmentSlug,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new Error('Failed to delete workspace environment');
|
throw new Error('Failed to create workspace environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === environmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments.splice(envIndex, 1);
|
||||||
|
await workspace.save();
|
||||||
|
|
||||||
// clean up
|
// clean up
|
||||||
await Secret.deleteMany({
|
await Secret.deleteMany({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment: environmentSlug,
|
environment: environmentSlug,
|
||||||
});
|
});
|
||||||
|
await SecretVersion.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
await ServiceToken.deleteMany({
|
await ServiceToken.deleteMany({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment: environmentSlug,
|
environment: environmentSlug,
|
||||||
});
|
});
|
||||||
|
await ServiceTokenData.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
await Integration.deleteMany({
|
await Integration.deleteMany({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment: environmentSlug,
|
environment: environmentSlug,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import { Schema, model, Types } from 'mongoose';
|
|||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD
|
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
|
|
||||||
export interface ISecretVersion {
|
export interface ISecretVersion {
|
||||||
@@ -56,7 +52,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
|||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDeleted: { // consider removing field
|
isDeleted: { // consider removing field
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
export interface IIntegration {
|
export interface IIntegration {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
environment: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
app: string;
|
app: string;
|
||||||
target: string;
|
target: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import express from 'express';
|
import express, { Response, Request } from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { body, param } from 'express-validator';
|
import { body, param } from 'express-validator';
|
||||||
import { environmentController } from '../../controllers/v2';
|
import { environmentController } from '../../controllers/v2';
|
||||||
@@ -7,14 +7,15 @@ import {
|
|||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
} from '../../middleware';
|
} from '../../middleware';
|
||||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
|
import { ADMIN, MEMBER } from '../../variables';
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/:workspaceId',
|
'/:workspaceId/environments',
|
||||||
requireAuth,
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt'],
|
||||||
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [COMPLETED, GRANTED],
|
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().trim(),
|
param('workspaceId').exists().trim(),
|
||||||
body('environmentSlug').exists().trim(),
|
body('environmentSlug').exists().trim(),
|
||||||
@@ -24,11 +25,12 @@ router.post(
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.put(
|
router.put(
|
||||||
'/:workspaceId',
|
'/:workspaceId/environments',
|
||||||
requireAuth,
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt'],
|
||||||
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [COMPLETED, GRANTED],
|
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().trim(),
|
param('workspaceId').exists().trim(),
|
||||||
body('environmentSlug').exists().trim(),
|
body('environmentSlug').exists().trim(),
|
||||||
@@ -39,11 +41,12 @@ router.put(
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
'/:workspaceId',
|
'/:workspaceId/environments',
|
||||||
requireAuth,
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt'],
|
||||||
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN],
|
acceptedRoles: [ADMIN],
|
||||||
acceptedStatuses: [GRANTED],
|
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().trim(),
|
param('workspaceId').exists().trim(),
|
||||||
body('environmentSlug').exists().trim(),
|
body('environmentSlug').exists().trim(),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
body('workspaceId').exists().isString().trim(),
|
body('workspaceId').exists().isString().trim(),
|
||||||
body('environment').exists().isString().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
body('environment').exists().isString().trim(),
|
||||||
body('secrets')
|
body('secrets')
|
||||||
.exists()
|
.exists()
|
||||||
.custom((value) => {
|
.custom((value) => {
|
||||||
@@ -73,7 +73,7 @@ router.post(
|
|||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/',
|
||||||
query('workspaceId').exists().trim(),
|
query('workspaceId').exists().trim(),
|
||||||
query('environment').exists().trim().isIn(['dev', 'staging', 'prod', 'test']),
|
query('environment').exists().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function ListBox({
|
|||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={personIdx}
|
key={personIdx}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected }) =>
|
||||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
|
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
|
||||||
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
||||||
} ${
|
} ${
|
||||||
active && !selected
|
active && !selected
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import nacl from "tweetnacl";
|
|||||||
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
|
||||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||||
|
|
||||||
import { envMapping } from "../../../public/data/frequentConstants";
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
encryptAssymmetric,
|
encryptAssymmetric,
|
||||||
@@ -34,11 +33,12 @@ const AddServiceTokenDialog = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
serviceTokens,
|
serviceTokens,
|
||||||
|
environments,
|
||||||
setServiceTokens
|
setServiceTokens
|
||||||
}) => {
|
}) => {
|
||||||
const [serviceToken, setServiceToken] = useState("");
|
const [serviceToken, setServiceToken] = useState("");
|
||||||
const [serviceTokenName, setServiceTokenName] = useState("");
|
const [serviceTokenName, setServiceTokenName] = useState("");
|
||||||
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development");
|
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
|
||||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
|
||||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -66,7 +66,7 @@ const AddServiceTokenDialog = ({
|
|||||||
let newServiceToken = await addServiceToken({
|
let newServiceToken = await addServiceToken({
|
||||||
name: serviceTokenName,
|
name: serviceTokenName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment: envMapping[serviceTokenEnv],
|
environment: selectedServiceTokenEnv.slug,
|
||||||
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
expiresIn: expiryMapping[serviceTokenExpiresIn],
|
||||||
encryptedKey: ciphertext,
|
encryptedKey: ciphertext,
|
||||||
iv,
|
iv,
|
||||||
@@ -101,155 +101,159 @@ const AddServiceTokenDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-50">
|
<div className='z-50'>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
<Dialog as='div' className='relative' onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter='ease-out duration-300'
|
||||||
enterFrom="opacity-0"
|
enterFrom='opacity-0'
|
||||||
enterTo="opacity-100"
|
enterTo='opacity-100'
|
||||||
leave="ease-in duration-200"
|
leave='ease-in duration-200'
|
||||||
leaveFrom="opacity-100"
|
leaveFrom='opacity-100'
|
||||||
leaveTo="opacity-0"
|
leaveTo='opacity-0'
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
|
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className='fixed inset-0 overflow-y-auto'>
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter='ease-out duration-300'
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom='opacity-0 scale-95'
|
||||||
enterTo="opacity-100 scale-100"
|
enterTo='opacity-100 scale-100'
|
||||||
leave="ease-in duration-200"
|
leave='ease-in duration-200'
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom='opacity-100 scale-100'
|
||||||
leaveTo="opacity-0 scale-95"
|
leaveTo='opacity-0 scale-95'
|
||||||
>
|
>
|
||||||
{serviceToken == "" ? (
|
{serviceToken == '' ? (
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as='h3'
|
||||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||||
>
|
>
|
||||||
{t("section-token:add-dialog.title", {
|
{t('section-token:add-dialog.title', {
|
||||||
target: workspaceName,
|
target: workspaceName,
|
||||||
})}
|
})}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2 mb-4">
|
<div className='mt-2 mb-4'>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<p className="text-sm text-gray-500">
|
<p className='text-sm text-gray-500'>
|
||||||
{t("section-token:add-dialog.description")}
|
{t('section-token:add-dialog.description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28 mb-2">
|
<div className='max-h-28 mb-2'>
|
||||||
<InputField
|
<InputField
|
||||||
label={t("section-token:add-dialog.name")}
|
label={t('section-token:add-dialog.name')}
|
||||||
onChangeHandler={setServiceTokenName}
|
onChangeHandler={setServiceTokenName}
|
||||||
type="varName"
|
type='varName'
|
||||||
value={serviceTokenName}
|
value={serviceTokenName}
|
||||||
placeholder=""
|
placeholder=''
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28 mb-2">
|
<div className='max-h-28 mb-2'>
|
||||||
<ListBox
|
<ListBox
|
||||||
selected={serviceTokenEnv}
|
selected={selectedServiceTokenEnv?.name}
|
||||||
onChange={setServiceTokenEnv}
|
data={environments.map(({ name }) => name)}
|
||||||
data={[
|
onChange={(envName) =>
|
||||||
"Development",
|
setSelectedServiceTokenEnv(
|
||||||
"Staging",
|
environments.find(
|
||||||
"Production",
|
({ name }) => envName === name
|
||||||
"Testing",
|
) || {
|
||||||
]}
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
isFull={true}
|
isFull={true}
|
||||||
text={`${t("common:environment")}: `}
|
text={`${t('common:environment')}: `}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-28">
|
<div className='max-h-28'>
|
||||||
<ListBox
|
<ListBox
|
||||||
selected={serviceTokenExpiresIn}
|
selected={serviceTokenExpiresIn}
|
||||||
onChange={setServiceTokenExpiresIn}
|
onChange={setServiceTokenExpiresIn}
|
||||||
data={[
|
data={[
|
||||||
"1 day",
|
'1 day',
|
||||||
"7 days",
|
'7 days',
|
||||||
"1 month",
|
'1 month',
|
||||||
"6 months",
|
'6 months',
|
||||||
"12 months",
|
'12 months',
|
||||||
]}
|
]}
|
||||||
isFull={true}
|
isFull={true}
|
||||||
text={`${t("common:expired-in")}: `}
|
text={`${t('common:expired-in')}: `}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-max">
|
<div className='max-w-max'>
|
||||||
<div className="mt-6 flex flex-col justify-start w-max">
|
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => generateServiceToken()}
|
onButtonPressed={() => generateServiceToken()}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
text={t("section-token:add-dialog.add")}
|
text={t('section-token:add-dialog.add')}
|
||||||
textDisabled={t("section-token:add-dialog.add")}
|
textDisabled={t('section-token:add-dialog.add')}
|
||||||
size="md"
|
size='md'
|
||||||
active={serviceTokenName == "" ? false : true}
|
active={serviceTokenName == '' ? false : true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
) : (
|
) : (
|
||||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as="h3"
|
as='h3'
|
||||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||||
>
|
>
|
||||||
{t("section-token:add-dialog.copy-service-token")}
|
{t('section-token:add-dialog.copy-service-token')}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2 mb-4">
|
<div className='mt-2 mb-4'>
|
||||||
<div className="flex flex-col">
|
<div className='flex flex-col'>
|
||||||
<p className="text-sm text-gray-500">
|
<p className='text-sm text-gray-500'>
|
||||||
{t(
|
{t(
|
||||||
"section-token:add-dialog.copy-service-token-description"
|
'section-token:add-dialog.copy-service-token-description'
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className='w-full'>
|
||||||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20">
|
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type='text'
|
||||||
value={serviceToken}
|
value={serviceToken}
|
||||||
id="serviceToken"
|
id='serviceToken'
|
||||||
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none"
|
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||||
></input>
|
></input>
|
||||||
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none">
|
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||||
{serviceToken}
|
{serviceToken}
|
||||||
</div>
|
</div>
|
||||||
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||||
<button
|
<button
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200"
|
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||||
>
|
>
|
||||||
{serviceTokenCopied ? (
|
{serviceTokenCopied ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCheck}
|
icon={faCheck}
|
||||||
className="pr-0.5"
|
className='pr-0.5'
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon icon={faCopy} />
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm">
|
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
|
||||||
{t("common:click-to-copy")}
|
{t('common:click-to-copy')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 flex flex-col justify-start w-max">
|
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => closeAddServiceTokenModal()}
|
onButtonPressed={() => closeAddServiceTokenModal()}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
text="Close"
|
text='Close'
|
||||||
size="md"
|
size='md'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
|||||||
+22
-4
@@ -12,15 +12,24 @@ type Props = {
|
|||||||
// on edit mode load up initial values
|
// on edit mode load up initial values
|
||||||
initialValues?: FormFields;
|
initialValues?: FormFields;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (envName: string, envSlug: string) => void;
|
onCreateSubmit: (data: FormFields) => void;
|
||||||
|
onEditSubmit: (data: FormFields) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Migrate to better form management and validation. Preferable react-hook-form + yup
|
||||||
/**
|
/**
|
||||||
* The dialog modal for when the user wants to create a new workspace
|
* The dialog modal for when the user wants to create a new workspace
|
||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues, isEditMode }: Props) => {
|
export const AddUpdateEnvironmentDialog = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onCreateSubmit,
|
||||||
|
onEditSubmit,
|
||||||
|
initialValues,
|
||||||
|
isEditMode,
|
||||||
|
}: Props) => {
|
||||||
const [formInput, setFormInput] = useState<FormFields>({
|
const [formInput, setFormInput] = useState<FormFields>({
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
@@ -39,7 +48,15 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues,
|
|||||||
|
|
||||||
const onFormSubmit: FormEventHandler = (e) => {
|
const onFormSubmit: FormEventHandler = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log(formInput);
|
const data = {
|
||||||
|
name: formInput.name.toLowerCase(),
|
||||||
|
slug: formInput.slug.toLowerCase(),
|
||||||
|
};
|
||||||
|
if (isEditMode) {
|
||||||
|
onEditSubmit(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onCreateSubmit(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,7 +98,7 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues,
|
|||||||
<form onSubmit={onFormSubmit}>
|
<form onSubmit={onFormSubmit}>
|
||||||
<div className='max-h-28 mt-4'>
|
<div className='max-h-28 mt-4'>
|
||||||
<InputField
|
<InputField
|
||||||
label='Project Name'
|
label='Environment Name'
|
||||||
onChangeHandler={(val) => onInputChange('name', val)}
|
onChangeHandler={(val) => onInputChange('name', val)}
|
||||||
type='varName'
|
type='varName'
|
||||||
value={formInput.name}
|
value={formInput.name}
|
||||||
@@ -112,6 +129,7 @@ export const AddEnvironmentDialog = ({ isOpen, onClose, onSubmit, initialValues,
|
|||||||
type='submit'
|
type='submit'
|
||||||
color='mineshaft'
|
color='mineshaft'
|
||||||
text={isEditMode ? 'Update' : 'Create'}
|
text={isEditMode ? 'Update' : 'Create'}
|
||||||
|
active={formInput.name !== '' && formInput.slug !== ''}
|
||||||
size='md'
|
size='md'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
import InputField from '../InputField';
|
import InputField from '../InputField';
|
||||||
@@ -21,6 +21,10 @@ const DeleteActionModal = ({
|
|||||||
}:Props) => {
|
}:Props) => {
|
||||||
const [deleteInputField, setDeleteInputField] = useState("")
|
const [deleteInputField, setDeleteInputField] = useState("")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDeleteInputField("");
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
|
|||||||
@@ -1,16 +1,62 @@
|
|||||||
import { faPencil,faPlus,faX } from '@fortawesome/free-solid-svg-icons';
|
import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import { usePopUp } from '../../../hooks/usePopUp';
|
import { usePopUp } from '../../../hooks/usePopUp';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
import {AddEnvironmentDialog} from '../dialog/AddEnvironmentDialog';
|
import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog';
|
||||||
import DeleteActionModal from '../dialog/DeleteActionModal';
|
import DeleteActionModal from '../dialog/DeleteActionModal';
|
||||||
|
|
||||||
const EnvironmentTable = ({ data = [] }) => {
|
type Env = { name: string; slug: string };
|
||||||
|
|
||||||
|
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([
|
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
'createUpdateEnv',
|
'createUpdateEnv',
|
||||||
'deleteEnv',
|
'deleteEnv',
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
|
const onEnvCreateCB = async (env: Env) => {
|
||||||
|
try {
|
||||||
|
await onCreateEnv(env);
|
||||||
|
handlePopUpClose('createUpdateEnv');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnvUpdateCB = async (env: Env) => {
|
||||||
|
try {
|
||||||
|
await onUpdateEnv(
|
||||||
|
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
|
||||||
|
env
|
||||||
|
);
|
||||||
|
handlePopUpClose('createUpdateEnv');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnvDeleteCB = async () => {
|
||||||
|
try {
|
||||||
|
await onDeleteEnv(
|
||||||
|
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
|
||||||
|
);
|
||||||
|
handlePopUpClose('deleteEnv');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex flex-row justify-between w-full'>
|
<div className='flex flex-row justify-between w-full'>
|
||||||
@@ -62,7 +108,9 @@ const EnvironmentTable = ({ data = [] }) => {
|
|||||||
<td className='py-2 border-mineshaft-700 border-t flex'>
|
<td className='py-2 border-mineshaft-700 border-t flex'>
|
||||||
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
|
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => handlePopUpOpen("createUpdateEnv",{ name, slug })}
|
onButtonPressed={() =>
|
||||||
|
handlePopUpOpen('createUpdateEnv', { name, slug })
|
||||||
|
}
|
||||||
color='red'
|
color='red'
|
||||||
size='icon-sm'
|
size='icon-sm'
|
||||||
icon={faPencil}
|
icon={faPencil}
|
||||||
@@ -101,14 +149,15 @@ const EnvironmentTable = ({ data = [] }) => {
|
|||||||
}?`}
|
}?`}
|
||||||
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
|
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
|
||||||
onClose={() => handlePopUpClose('deleteEnv')}
|
onClose={() => handlePopUpClose('deleteEnv')}
|
||||||
onSubmit={() => handlePopUpClose('deleteEnv')}
|
onSubmit={onEnvDeleteCB}
|
||||||
/>
|
/>
|
||||||
<AddEnvironmentDialog
|
<AddUpdateEnvironmentDialog
|
||||||
isOpen={popUp.createUpdateEnv.isOpen}
|
isOpen={popUp.createUpdateEnv.isOpen}
|
||||||
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
|
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
|
||||||
initialValues={popUp?.createUpdateEnv?.data as any}
|
initialValues={popUp?.createUpdateEnv?.data as any}
|
||||||
onClose={() => handlePopUpClose('createUpdateEnv')}
|
onClose={() => handlePopUpClose('createUpdateEnv')}
|
||||||
onSubmit={() => null}
|
onCreateSubmit={onEnvCreateCB}
|
||||||
|
onEditSubmit={onEnvUpdateCB}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { faX } from '@fortawesome/free-solid-svg-icons';
|
|||||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||||
|
|
||||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||||
import { reverseEnvMapping } from '../../../public/data/frequentConstants';
|
|
||||||
import guidGenerator from '../../utilities/randomId';
|
import guidGenerator from '../../utilities/randomId';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
|
|
||||||
@@ -60,7 +59,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
|||||||
{workspaceName}
|
{workspaceName}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
{reverseEnvMapping[row.environment]}
|
{row.environment}
|
||||||
</td>
|
</td>
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
{new Date(row.expiresAt).toUTCString()}
|
{new Date(row.expiresAt).toUTCString()}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"
|
|||||||
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
||||||
import {
|
import {
|
||||||
contextNetlifyMapping,
|
contextNetlifyMapping,
|
||||||
envMapping,
|
|
||||||
reverseContextNetlifyMapping,
|
reverseContextNetlifyMapping,
|
||||||
reverseEnvMapping,
|
|
||||||
} from "../../public/data/frequentConstants";
|
} from "../../public/data/frequentConstants";
|
||||||
|
|
||||||
interface Integration {
|
interface Integration {
|
||||||
@@ -41,9 +39,7 @@ const Integration = ({
|
|||||||
}: {
|
}: {
|
||||||
integration: Integration;
|
integration: Integration;
|
||||||
}) => {
|
}) => {
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState(
|
const [integrationEnvironment, setIntegrationEnvironment] = useState(integration.environment);
|
||||||
reverseEnvMapping[integration.environment]
|
|
||||||
);
|
|
||||||
const [fileState, setFileState] = useState([]);
|
const [fileState, setFileState] = useState([]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||||
@@ -199,9 +195,9 @@ const Integration = ({
|
|||||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||||
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
||||||
|
|
||||||
const result = await updateIntegration({
|
await updateIntegration({
|
||||||
integrationId: integration._id,
|
integrationId: integration._id,
|
||||||
environment: envMapping[integrationEnvironment],
|
environment: integrationEnvironment,
|
||||||
app: integrationApp,
|
app: integrationApp,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { envMapping } from "../../../public/data/frequentConstants";
|
|
||||||
import checkOverrides from './checkOverrides';
|
import checkOverrides from './checkOverrides';
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
|
|||||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||||
const alink = document.createElement('a');
|
const alink = document.createElement('a');
|
||||||
alink.href = fileDownloadUrl;
|
alink.href = fileDownloadUrl;
|
||||||
alink.download = envMapping[env] + '.env';
|
alink.download = env + '.env';
|
||||||
alink.click();
|
alink.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import getSecrets from '~/pages/api/files/GetSecrets';
|
import getSecrets from '~/pages/api/files/GetSecrets';
|
||||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||||
|
|
||||||
import { envMapping } from '../../../public/data/frequentConstants';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
decryptSymmetric
|
decryptSymmetric
|
||||||
@@ -35,7 +33,7 @@ interface SecretProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FunctionProps {
|
interface FunctionProps {
|
||||||
env: keyof typeof envMapping;
|
env: string;
|
||||||
setIsKeyAvailable: any;
|
setIsKeyAvailable: any;
|
||||||
setData: any;
|
setData: any;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -58,7 +56,7 @@ const getSecretsForProject = async ({
|
|||||||
try {
|
try {
|
||||||
let encryptedSecrets;
|
let encryptedSecrets;
|
||||||
try {
|
try {
|
||||||
encryptedSecrets = await getSecrets(workspaceId, envMapping[env]);
|
encryptedSecrets = await getSecrets(workspaceId, env);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('ERROR: Not able to access the latest version of secrets');
|
console.log('ERROR: Not able to access the latest version of secrets');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
|
||||||
|
type NewEnvironmentInfo = {
|
||||||
|
environmentSlug: string;
|
||||||
|
environmentName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route deletes a specified workspace.
|
||||||
|
* @param {*} workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const createEnvironment = (workspaceId:string, newEnv: NewEnvironmentInfo) => {
|
||||||
|
return SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(newEnv)
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
console.log('Failed to create environment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createEnvironment;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
/**
|
||||||
|
* This route deletes a specified env.
|
||||||
|
* @param {*} workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const deleteEnvironment = (workspaceId: string, environmentSlug: string) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
`/api/v2/workspace/${workspaceId}/environments`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ environmentSlug }),
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
console.log('Failed to delete environment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteEnvironment;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import SecurityClient from '~/utilities/SecurityClient';
|
||||||
|
|
||||||
|
type EnvironmentInfo = {
|
||||||
|
oldEnvironmentSlug: string;
|
||||||
|
environmentSlug: string;
|
||||||
|
environmentName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This route updates a specified environment.
|
||||||
|
* @param {*} workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const updateEnvironment = (workspaceId: string, env: EnvironmentInfo) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
`/api/v2/workspace/${workspaceId}/environments`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(env),
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res && res.status == 200) {
|
||||||
|
return res;
|
||||||
|
} else {
|
||||||
|
console.log('Failed to update environment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateEnvironment;
|
||||||
@@ -5,6 +5,7 @@ interface Workspace {
|
|||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
|
environments: Array<{name:string, slug:string}>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+382
-179
@@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useState } from 'react';
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import {
|
import {
|
||||||
faArrowDownAZ,
|
faArrowDownAZ,
|
||||||
faArrowDownZA,
|
faArrowDownZA,
|
||||||
@@ -34,7 +34,6 @@ import getSecretsForProject from '~/components/utilities/secrets/getSecretsForPr
|
|||||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||||
import guidGenerator from '~/utilities/randomId';
|
import guidGenerator from '~/utilities/randomId';
|
||||||
|
|
||||||
import { envMapping, reverseEnvMapping } from '../../public/data/frequentConstants';
|
|
||||||
import addSecrets from '../api/files/AddSecrets';
|
import addSecrets from '../api/files/AddSecrets';
|
||||||
import deleteSecrets from '../api/files/DeleteSecrets';
|
import deleteSecrets from '../api/files/DeleteSecrets';
|
||||||
import updateSecrets from '../api/files/UpdateSecrets';
|
import updateSecrets from '../api/files/UpdateSecrets';
|
||||||
@@ -43,6 +42,10 @@ import checkUserAction from '../api/userActions/checkUserAction';
|
|||||||
import registerUserAction from '../api/userActions/registerUserAction';
|
import registerUserAction from '../api/userActions/registerUserAction';
|
||||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||||
|
|
||||||
|
type WorkspaceEnv = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface SecretDataProps {
|
interface SecretDataProps {
|
||||||
type: 'personal' | 'shared';
|
type: 'personal' | 'shared';
|
||||||
@@ -68,7 +71,7 @@ interface SnapshotProps {
|
|||||||
secretVersions: {
|
secretVersions: {
|
||||||
id: string;
|
id: string;
|
||||||
pos: number;
|
pos: number;
|
||||||
type: "personal" | "shared";
|
type: 'personal' | 'shared';
|
||||||
environment: string;
|
environment: string;
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -102,11 +105,8 @@ export default function Dashboard() {
|
|||||||
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
|
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
const [buttonReady, setButtonReady] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [workspaceId, setWorkspaceId] = useState('');
|
|
||||||
const [blurred, setBlurred] = useState(true);
|
const [blurred, setBlurred] = useState(true);
|
||||||
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
|
||||||
const [env, setEnv] = useState('Development');
|
|
||||||
const [snapshotEnv, setSnapshotEnv] = useState('Development');
|
|
||||||
const [isNew, setIsNew] = useState(false);
|
const [isNew, setIsNew] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [searchKeys, setSearchKeys] = useState('');
|
const [searchKeys, setSearchKeys] = useState('');
|
||||||
@@ -114,7 +114,7 @@ export default function Dashboard() {
|
|||||||
const [sortMethod, setSortMethod] = useState('alphabetical');
|
const [sortMethod, setSortMethod] = useState('alphabetical');
|
||||||
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
|
||||||
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
|
||||||
const [sidebarSecretId, toggleSidebar] = useState("None");
|
const [sidebarSecretId, toggleSidebar] = useState('None');
|
||||||
const [PITSidebarOpen, togglePITSidebar] = useState(false);
|
const [PITSidebarOpen, togglePITSidebar] = useState(false);
|
||||||
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
|
||||||
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
|
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
|
||||||
@@ -123,6 +123,16 @@ export default function Dashboard() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const workspaceId = router.query.id as string;
|
||||||
|
const [workspaceEnvs, setWorkspaceEnvs] = useState<WorkspaceEnv[]>([]);
|
||||||
|
|
||||||
|
const [selectedSnapshotEnv, setSelectedSnapshotEnv] =
|
||||||
|
useState<WorkspaceEnv>();
|
||||||
|
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv>({
|
||||||
|
name: '',
|
||||||
|
slug: '',
|
||||||
|
});
|
||||||
|
|
||||||
// #TODO: fix save message for changing reroutes
|
// #TODO: fix save message for changing reroutes
|
||||||
// const beforeRouteHandler = (url) => {
|
// const beforeRouteHandler = (url) => {
|
||||||
// const warningText =
|
// const warningText =
|
||||||
@@ -169,25 +179,37 @@ export default function Dashboard() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const tempNumSnapshots = await getProjectSercetSnapshotsCount({ workspaceId: String(router.query.id) })
|
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
setNumSnapshots(tempNumSnapshots);
|
setNumSnapshots(tempNumSnapshots);
|
||||||
const userWorkspaces = await getWorkspaces();
|
const userWorkspaces = await getWorkspaces();
|
||||||
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
|
const workspace = userWorkspaces.find(
|
||||||
if (
|
(workspace) => workspace._id === workspaceId
|
||||||
!listWorkspaces.includes(router.asPath.split('/')[2])
|
);
|
||||||
) {
|
if (!workspace) {
|
||||||
router.push('/dashboard/' + listWorkspaces[0]);
|
router.push('/dashboard/' + userWorkspaces?.[0]?._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setWorkspaceEnvs(workspace?.environments || []);
|
||||||
|
// set env
|
||||||
|
const env = workspace?.environments?.[0] || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unkown',
|
||||||
|
};
|
||||||
|
setSelectedEnv(env);
|
||||||
|
setSelectedSnapshotEnv(env);
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
setIsNew(
|
setIsNew(
|
||||||
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
|
(Date.parse(String(new Date())) - Date.parse(user.createdAt)) /
|
||||||
|
60000 <
|
||||||
|
3
|
||||||
? true
|
? true
|
||||||
: false
|
: false
|
||||||
);
|
);
|
||||||
|
|
||||||
const userAction = await checkUserAction({
|
const userAction = await checkUserAction({
|
||||||
action: 'first_time_secrets_pushed'
|
action: 'first_time_secrets_pushed',
|
||||||
});
|
});
|
||||||
setHasUserEverPushed(userAction ? true : false);
|
setHasUserEverPushed(userAction ? true : false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -202,13 +224,12 @@ export default function Dashboard() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setBlurred(true);
|
setBlurred(true);
|
||||||
setWorkspaceId(String(router.query.id));
|
// ENV
|
||||||
|
|
||||||
const dataToSort = await getSecretsForProject({
|
const dataToSort = await getSecretsForProject({
|
||||||
env,
|
env: selectedEnv.slug,
|
||||||
setIsKeyAvailable,
|
setIsKeyAvailable,
|
||||||
setData,
|
setData,
|
||||||
workspaceId: String(router.query.id)
|
workspaceId,
|
||||||
});
|
});
|
||||||
setInitialData(dataToSort);
|
setInitialData(dataToSort);
|
||||||
reorderRows(dataToSort);
|
reorderRows(dataToSort);
|
||||||
@@ -230,7 +251,7 @@ export default function Dashboard() {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [env]);
|
}, [selectedEnv]);
|
||||||
|
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
setIsNew(false);
|
setIsNew(false);
|
||||||
@@ -243,7 +264,7 @@ export default function Dashboard() {
|
|||||||
value: '',
|
value: '',
|
||||||
type: 'shared',
|
type: 'shared',
|
||||||
comment: '',
|
comment: '',
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -265,20 +286,32 @@ export default function Dashboard() {
|
|||||||
key: keyName,
|
key: keyName,
|
||||||
value: value,
|
value: value,
|
||||||
type: 'personal',
|
type: 'personal',
|
||||||
comment: comment
|
comment: comment,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
sortValuesHandler(
|
||||||
|
tempdata,
|
||||||
|
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
const deleteRow = ({
|
||||||
|
ids,
|
||||||
|
secretName,
|
||||||
|
}: {
|
||||||
|
ids: string[];
|
||||||
|
secretName: string;
|
||||||
|
}) => {
|
||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
toggleSidebar("None");
|
toggleSidebar('None');
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `${secretName} has been deleted. Remember to save changes.`,
|
text: `${secretName} has been deleted. Remember to save changes.`,
|
||||||
type: 'error'
|
type: 'error',
|
||||||
});
|
});
|
||||||
sortValuesHandler(data!.filter((row: SecretDataProps) => !ids.includes(row.id)), sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
|
sortValuesHandler(
|
||||||
|
data!.filter((row: SecretDataProps) => !ids.includes(row.id)),
|
||||||
|
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -289,15 +322,30 @@ export default function Dashboard() {
|
|||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
|
|
||||||
// find which shared secret corresponds to the overriden version
|
// find which shared secret corresponds to the overriden version
|
||||||
const sharedVersionOfOverride = data!.filter(secret => secret.type == "shared" && secret.key == data!.filter(row => row.id == id)[0]?.key)[0]?.id;
|
const sharedVersionOfOverride = data!.filter(
|
||||||
|
(secret) =>
|
||||||
|
secret.type == 'shared' &&
|
||||||
|
secret.key == data!.filter((row) => row.id == id)[0]?.key
|
||||||
|
)[0]?.id;
|
||||||
|
|
||||||
// change the sidebar to this shared secret; and unhide it
|
// change the sidebar to this shared secret; and unhide it
|
||||||
toggleSidebar(sharedVersionOfOverride)
|
toggleSidebar(sharedVersionOfOverride);
|
||||||
setSharedToHide(sharedToHide!.filter(tempId => tempId != sharedVersionOfOverride))
|
setSharedToHide(
|
||||||
|
sharedToHide!.filter((tempId) => tempId != sharedVersionOfOverride)
|
||||||
|
);
|
||||||
|
|
||||||
// resort secrets
|
// resort secrets
|
||||||
const tempData = data!.filter((row: SecretDataProps) => !(row.key == data!.filter(row => row.id == id)[0]?.key && row.type == 'personal'))
|
const tempData = data!.filter(
|
||||||
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
|
(row: SecretDataProps) =>
|
||||||
|
!(
|
||||||
|
row.key == data!.filter((row) => row.id == id)[0]?.key &&
|
||||||
|
row.type == 'personal'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
sortValuesHandler(
|
||||||
|
tempData,
|
||||||
|
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyValue = (value: string, pos: number) => {
|
const modifyValue = (value: string, pos: number) => {
|
||||||
@@ -351,59 +399,97 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
const obj = Object.assign(
|
const obj = Object.assign(
|
||||||
{},
|
{},
|
||||||
...newData!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''] }))
|
...newData!.map((row: SecretDataProps) => ({
|
||||||
|
[row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''],
|
||||||
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Checking if any of the secret keys start with a number - if so, don't do anything
|
// Checking if any of the secret keys start with a number - if so, don't do anything
|
||||||
const nameErrors = !Object.keys(obj)
|
const nameErrors = !Object.keys(obj)
|
||||||
.map((key) => !isNaN(Number(key[0].charAt(0))))
|
.map((key) => !isNaN(Number(key[0].charAt(0))))
|
||||||
.every((v) => v === false);
|
.every((v) => v === false);
|
||||||
const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0;
|
const duplicatesExist =
|
||||||
|
findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type))
|
||||||
|
.length > 0;
|
||||||
|
|
||||||
if (nameErrors) {
|
if (nameErrors) {
|
||||||
return createNotification({
|
return createNotification({
|
||||||
text: 'Solve all name errors before saving secrets.',
|
text: 'Solve all name errors before saving secrets.',
|
||||||
type: 'error'
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (duplicatesExist) {
|
if (duplicatesExist) {
|
||||||
return createNotification({
|
return createNotification({
|
||||||
text: 'Remove duplicated secret names before saving.',
|
text: 'Remove duplicated secret names before saving.',
|
||||||
type: 'error'
|
type: 'error',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once "Save changes" is clicked, disable that button
|
// Once "Save changes" is clicked, disable that button
|
||||||
setButtonReady(false);
|
setButtonReady(false);
|
||||||
|
|
||||||
const secretsToBeDeleted
|
const secretsToBeDeleted = initialData
|
||||||
= initialData
|
.filter(
|
||||||
.filter(initDataPoint => !newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id))
|
(initDataPoint) =>
|
||||||
.map(secret => secret.id);
|
!newData!
|
||||||
|
.map((newDataPoint) => newDataPoint.id)
|
||||||
|
.includes(initDataPoint.id)
|
||||||
|
)
|
||||||
|
.map((secret) => secret.id);
|
||||||
|
|
||||||
const secretsToBeAdded
|
const secretsToBeAdded = newData!.filter(
|
||||||
= newData!
|
(newDataPoint) =>
|
||||||
.filter(newDataPoint => !initialData.map(initDataPoint => initDataPoint.id).includes(newDataPoint.id));
|
!initialData
|
||||||
|
.map((initDataPoint) => initDataPoint.id)
|
||||||
|
.includes(newDataPoint.id)
|
||||||
|
);
|
||||||
|
|
||||||
const secretsToBeUpdated
|
const secretsToBeUpdated = newData!.filter((newDataPoint) =>
|
||||||
= newData!.filter(newDataPoint => initialData
|
initialData
|
||||||
.filter(initDataPoint => newData!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
|
.filter(
|
||||||
&& (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value
|
(initDataPoint) =>
|
||||||
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
|
newData!
|
||||||
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
|
.map((newDataPoint) => newDataPoint.id)
|
||||||
.map(secret => secret.id).includes(newDataPoint.id));
|
.includes(initDataPoint.id) &&
|
||||||
|
(newData!.filter(
|
||||||
|
(newDataPoint) => newDataPoint.id == initDataPoint.id
|
||||||
|
)[0].value != initDataPoint.value ||
|
||||||
|
newData!.filter(
|
||||||
|
(newDataPoint) => newDataPoint.id == initDataPoint.id
|
||||||
|
)[0].key != initDataPoint.key ||
|
||||||
|
newData!.filter(
|
||||||
|
(newDataPoint) => newDataPoint.id == initDataPoint.id
|
||||||
|
)[0].comment != initDataPoint.comment)
|
||||||
|
)
|
||||||
|
.map((secret) => secret.id)
|
||||||
|
.includes(newDataPoint.id)
|
||||||
|
);
|
||||||
|
|
||||||
if (secretsToBeDeleted.length > 0) {
|
if (secretsToBeDeleted.length > 0) {
|
||||||
await deleteSecrets({ secretIds: secretsToBeDeleted });
|
await deleteSecrets({ secretIds: secretsToBeDeleted });
|
||||||
}
|
}
|
||||||
|
// ENV
|
||||||
if (secretsToBeAdded.length > 0) {
|
if (secretsToBeAdded.length > 0) {
|
||||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId, env: envMapping[env] })
|
const secrets = await encryptSecrets({
|
||||||
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId });
|
secretsToEncrypt: secretsToBeAdded,
|
||||||
|
workspaceId,
|
||||||
|
env: selectedEnv.slug,
|
||||||
|
});
|
||||||
|
secrets &&
|
||||||
|
(await addSecrets({
|
||||||
|
secrets,
|
||||||
|
env: selectedEnv.slug,
|
||||||
|
workspaceId,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
if (secretsToBeUpdated.length > 0) {
|
if (secretsToBeUpdated.length > 0) {
|
||||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated, workspaceId, env: envMapping[env] })
|
const secrets = await encryptSecrets({
|
||||||
secrets && await updateSecrets({ secrets });
|
secretsToEncrypt: secretsToBeUpdated,
|
||||||
|
workspaceId,
|
||||||
|
env: selectedEnv.slug,
|
||||||
|
});
|
||||||
|
secrets && (await updateSecrets({ secrets }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||||
@@ -425,8 +511,12 @@ export default function Dashboard() {
|
|||||||
setBlurred(!blurred);
|
setBlurred(!blurred);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => {
|
const sortValuesHandler = (
|
||||||
const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
dataToSort: SecretDataProps[] | 1,
|
||||||
|
specificSortMethod?: 'alphabetical' | '-alphabetical'
|
||||||
|
) => {
|
||||||
|
const howToSort =
|
||||||
|
specificSortMethod == undefined ? sortMethod : specificSortMethod;
|
||||||
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
const sortedData = (dataToSort != 1 ? dataToSort : data)!
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
howToSort == 'alphabetical'
|
howToSort == 'alphabetical'
|
||||||
@@ -436,30 +526,44 @@ export default function Dashboard() {
|
|||||||
.map((item: SecretDataProps, index: number) => {
|
.map((item: SecretDataProps, index: number) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
pos: index
|
pos: index,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setData(sortedData);
|
setData(sortedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
|
const deleteCertainRow = ({
|
||||||
deleteRow({ids, secretName});
|
ids,
|
||||||
|
secretName,
|
||||||
|
}: {
|
||||||
|
ids: string[];
|
||||||
|
secretName: string;
|
||||||
|
}) => {
|
||||||
|
deleteRow({ ids, secretName });
|
||||||
};
|
};
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{t("common:head-title", { title: t("dashboard:title") })}</title>
|
<title>{t('common:head-title', { title: t('dashboard:title') })}</title>
|
||||||
<link rel="icon" href="/infisical.ico" />
|
<link rel='icon' href='/infisical.ico' />
|
||||||
<meta property="og:image" content="/images/message.png" />
|
<meta property='og:image' content='/images/message.png' />
|
||||||
<meta property="og:title" content={String(t("dashboard:og-title"))} />
|
<meta property='og:title' content={String(t('dashboard:og-title'))} />
|
||||||
<meta name="og:description" content={String(t("dashboard:og-description"))} />
|
<meta
|
||||||
|
name='og:description'
|
||||||
|
content={String(t('dashboard:og-description'))}
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-row">
|
<div className='flex flex-row'>
|
||||||
{sidebarSecretId != "None" && <SideBar
|
{sidebarSecretId != 'None' && (
|
||||||
|
<SideBar
|
||||||
toggleSidebar={toggleSidebar}
|
toggleSidebar={toggleSidebar}
|
||||||
data={data.filter((row: SecretDataProps) => row.key == data.filter(row => row.id == sidebarSecretId)[0]?.key)}
|
data={data.filter(
|
||||||
|
(row: SecretDataProps) =>
|
||||||
|
row.key ==
|
||||||
|
data.filter((row) => row.id == sidebarSecretId)[0]?.key
|
||||||
|
)}
|
||||||
modifyKey={listenChangeKey}
|
modifyKey={listenChangeKey}
|
||||||
modifyValue={listenChangeValue}
|
modifyValue={listenChangeValue}
|
||||||
modifyComment={listenChangeComment}
|
modifyComment={listenChangeComment}
|
||||||
@@ -470,200 +574,274 @@ export default function Dashboard() {
|
|||||||
sharedToHide={sharedToHide}
|
sharedToHide={sharedToHide}
|
||||||
setSharedToHide={setSharedToHide}
|
setSharedToHide={setSharedToHide}
|
||||||
deleteRow={deleteCertainRow}
|
deleteRow={deleteCertainRow}
|
||||||
/>}
|
/>
|
||||||
{PITSidebarOpen && <PITRecoverySidebar
|
)}
|
||||||
|
{PITSidebarOpen && (
|
||||||
|
<PITRecoverySidebar
|
||||||
toggleSidebar={togglePITSidebar}
|
toggleSidebar={togglePITSidebar}
|
||||||
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : "")}
|
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
|
||||||
setSnapshotData={setSnapshotData}
|
setSnapshotData={setSnapshotData}
|
||||||
/>}
|
/>
|
||||||
<div className="w-full max-h-96 pb-2">
|
)}
|
||||||
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
|
<div className='w-full max-h-96 pb-2'>
|
||||||
|
<NavHeader pageName={t('dashboard:title')} isProjectRelated={true} />
|
||||||
{checkDocsPopUpVisible && (
|
{checkDocsPopUpVisible && (
|
||||||
<BottonRightPopup
|
<BottonRightPopup
|
||||||
buttonText={t("dashboard:check-docs.button")}
|
buttonText={t('dashboard:check-docs.button')}
|
||||||
buttonLink="https://infisical.com/docs/getting-started/introduction"
|
buttonLink='https://infisical.com/docs/getting-started/introduction'
|
||||||
titleText={t("dashboard:check-docs.title")}
|
titleText={t('dashboard:check-docs.title')}
|
||||||
emoji="🎉"
|
emoji='🎉'
|
||||||
textLine1={t("dashboard:check-docs.line1")}
|
textLine1={t('dashboard:check-docs.line1')}
|
||||||
textLine2={t("dashboard:check-docs.line2")}
|
textLine2={t('dashboard:check-docs.line2')}
|
||||||
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
|
setCheckDocsPopUpVisible={setCheckDocsPopUpVisible}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl">
|
<div className='flex flex-row justify-between items-center mx-6 mt-6 mb-3 text-xl max-w-5xl'>
|
||||||
{snapshotData &&
|
{snapshotData && (
|
||||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||||
<Button
|
<Button
|
||||||
text={String(t("Go back to current"))}
|
text={String(t('Go back to current'))}
|
||||||
onButtonPressed={() => setSnapshotData(undefined)}
|
onButtonPressed={() => setSnapshotData(undefined)}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
size="md"
|
size='md'
|
||||||
icon={faArrowLeft}
|
icon={faArrowLeft}
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>
|
||||||
<div className="flex flex-row justify-start items-center text-3xl">
|
)}
|
||||||
<div className="font-semibold mr-4 mt-1 flex flex-row items-center">
|
<div className='flex flex-row justify-start items-center text-3xl'>
|
||||||
<p>{snapshotData ? "Secret Snapshot" : t("dashboard:title")}</p>
|
<div className='font-semibold mr-4 mt-1 flex flex-row items-center'>
|
||||||
{snapshotData && <span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>{new Date(snapshotData.createdAt).toLocaleString()}</span>}
|
<p>{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}</p>
|
||||||
|
{snapshotData && (
|
||||||
|
<span className='bg-primary-800 text-sm ml-4 mt-1 px-1.5 rounded-md'>
|
||||||
|
{new Date(snapshotData.createdAt).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && data?.length == 0 && (
|
{!snapshotData && data?.length == 0 && (
|
||||||
<ListBox
|
<ListBox
|
||||||
selected={env}
|
selected={selectedEnv.name}
|
||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={setEnv}
|
onChange={(envName) =>
|
||||||
|
setSelectedEnv(
|
||||||
|
workspaceEnvs.find(({ name }) => envName === name) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className='flex flex-row'>
|
||||||
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
<div className={`flex justify-start max-w-sm mt-1 mr-2`}>
|
||||||
<Button
|
<Button
|
||||||
text={String(numSnapshots + " " + t("Commits"))}
|
text={String(numSnapshots + ' ' + t('Commits'))}
|
||||||
onButtonPressed={() => togglePITSidebar(true)}
|
onButtonPressed={() => togglePITSidebar(true)}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
size="md"
|
size='md'
|
||||||
icon={faClockRotateLeft}
|
icon={faClockRotateLeft}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
{(data?.length !== 0 || buttonReady) && !snapshotData && (
|
||||||
<div className={`flex justify-start max-w-sm mt-1`}>
|
<div className={`flex justify-start max-w-sm mt-1`}>
|
||||||
<Button
|
<Button
|
||||||
text={String(t("common:save-changes"))}
|
text={String(t('common:save-changes'))}
|
||||||
onButtonPressed={savePush}
|
onButtonPressed={savePush}
|
||||||
color="primary"
|
color='primary'
|
||||||
size="md"
|
size='md'
|
||||||
active={buttonReady}
|
active={buttonReady}
|
||||||
iconDisabled={faCheck}
|
iconDisabled={faCheck}
|
||||||
textDisabled={String(t("common:saved"))}
|
textDisabled={String(t('common:saved'))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{snapshotData && <div className={`flex justify-start max-w-sm mt-1`}>
|
{snapshotData && (
|
||||||
|
<div className={`flex justify-start max-w-sm mt-1`}>
|
||||||
<Button
|
<Button
|
||||||
text={String(t("Rollback to this snapshot"))}
|
text={String(t('Rollback to this snapshot'))}
|
||||||
onButtonPressed={async () => {
|
onButtonPressed={async () => {
|
||||||
// Update secrets in the state only for the current environment
|
// Update secrets in the state only for the current environment
|
||||||
const rolledBackSecrets = snapshotData.secretVersions
|
const rolledBackSecrets = snapshotData.secretVersions
|
||||||
.filter(row => reverseEnvMapping[row.environment] == env)
|
.filter((row) => row.environment == selectedEnv.slug)
|
||||||
.map((sv, position) => {
|
.map((sv, position) => {
|
||||||
return {
|
return {
|
||||||
id: sv.id, pos: position, type: sv.type, key: sv.key, value: sv.value, comment: ''
|
id: sv.id,
|
||||||
}
|
pos: position,
|
||||||
|
type: sv.type,
|
||||||
|
key: sv.key,
|
||||||
|
value: sv.value,
|
||||||
|
comment: '',
|
||||||
|
};
|
||||||
});
|
});
|
||||||
setData(rolledBackSecrets);
|
setData(rolledBackSecrets);
|
||||||
|
|
||||||
setSharedToHide(
|
setSharedToHide(
|
||||||
rolledBackSecrets?.filter(row => (rolledBackSecrets
|
rolledBackSecrets
|
||||||
|
?.filter(
|
||||||
|
(row) =>
|
||||||
|
rolledBackSecrets
|
||||||
?.map((item) => item.key)
|
?.map((item) => item.key)
|
||||||
.filter(
|
.filter(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
index !==
|
index !==
|
||||||
rolledBackSecrets?.map((item) => item.key).indexOf(item)
|
rolledBackSecrets
|
||||||
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
|
?.map((item) => item.key)
|
||||||
|
.indexOf(item)
|
||||||
)
|
)
|
||||||
|
.includes(row.key) && row.type == 'shared'
|
||||||
|
)
|
||||||
|
?.map((item) => item.id)
|
||||||
|
);
|
||||||
|
|
||||||
// Perform the rollback globally
|
// Perform the rollback globally
|
||||||
performSecretRollback({ workspaceId, version: snapshotData.version })
|
performSecretRollback({
|
||||||
|
workspaceId,
|
||||||
|
version: snapshotData.version,
|
||||||
|
});
|
||||||
|
|
||||||
setSnapshotData(undefined);
|
setSnapshotData(undefined);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: `Rollback has been performed successfully.`,
|
text: `Rollback has been performed successfully.`,
|
||||||
type: 'success'
|
type: 'success',
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
color="primary"
|
color='primary'
|
||||||
size="md"
|
size='md'
|
||||||
active={buttonReady}
|
active={buttonReady}
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-6 w-full pr-12">
|
<div className='mx-6 w-full pr-12'>
|
||||||
<div className="flex flex-col max-w-5xl pb-1">
|
<div className='flex flex-col max-w-5xl pb-1'>
|
||||||
<div className="w-full flex flex-row items-start">
|
<div className='w-full flex flex-row items-start'>
|
||||||
{(snapshotData || data?.length !== 0) && (
|
{(snapshotData || data?.length !== 0) && (
|
||||||
<>
|
<>
|
||||||
{!snapshotData
|
{!snapshotData ? (
|
||||||
? <ListBox
|
<ListBox
|
||||||
selected={env}
|
selected={selectedEnv.name}
|
||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
onChange={setEnv}
|
onChange={(envName) =>
|
||||||
|
setSelectedEnv(
|
||||||
|
workspaceEnvs.find(
|
||||||
|
({ name }) => envName === name
|
||||||
|
) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
: <ListBox
|
) : (
|
||||||
selected={snapshotEnv}
|
<ListBox
|
||||||
data={['Development', 'Staging', 'Production', 'Testing']}
|
selected={selectedSnapshotEnv?.name || ''}
|
||||||
onChange={setSnapshotEnv}
|
data={workspaceEnvs.map(({ name }) => name)}
|
||||||
/>}
|
onChange={(envName) =>
|
||||||
<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">
|
setSelectedSnapshotEnv(
|
||||||
|
workspaceEnvs.find(
|
||||||
|
({ name }) => envName === name
|
||||||
|
) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className='h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center'>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
className='bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400'
|
||||||
icon={faMagnifyingGlass}
|
icon={faMagnifyingGlass}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
|
className='pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none'
|
||||||
value={searchKeys}
|
value={searchKeys}
|
||||||
onChange={(e) => setSearchKeys(e.target.value)}
|
onChange={(e) => setSearchKeys(e.target.value)}
|
||||||
placeholder={String(t("dashboard:search-keys"))}
|
placeholder={String(t('dashboard:search-keys'))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
{!snapshotData && (
|
||||||
|
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() => reorderRows(1)}
|
onButtonPressed={() => reorderRows(1)}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
size="icon-md"
|
size='icon-md'
|
||||||
icon={
|
icon={
|
||||||
sortMethod == 'alphabetical'
|
sortMethod == 'alphabetical'
|
||||||
? faArrowDownAZ
|
? faArrowDownAZ
|
||||||
: faArrowDownZA
|
: faArrowDownZA
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>
|
||||||
{!snapshotData && <div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
)}
|
||||||
<DownloadSecretMenu data={data} env={env} />
|
{!snapshotData && (
|
||||||
</div>}
|
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
|
||||||
<div className="ml-2 min-w-max flex flex-row items-start justify-start">
|
<DownloadSecretMenu
|
||||||
|
data={data}
|
||||||
|
env={selectedEnv.slug}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='ml-2 min-w-max flex flex-row items-start justify-start'>
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={changeBlurred}
|
onButtonPressed={changeBlurred}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
size="icon-md"
|
size='icon-md'
|
||||||
icon={blurred ? faEye : faEyeSlash}
|
icon={blurred ? faEye : faEyeSlash}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && <div className="relative ml-2 min-w-max flex flex-row items-start justify-end">
|
{!snapshotData && (
|
||||||
|
<div className='relative ml-2 min-w-max flex flex-row items-start justify-end'>
|
||||||
<Button
|
<Button
|
||||||
text={String(t("dashboard:add-key"))}
|
text={String(t('dashboard:add-key'))}
|
||||||
onButtonPressed={addRow}
|
onButtonPressed={addRow}
|
||||||
color="mineshaft"
|
color='mineshaft'
|
||||||
icon={faPlus}
|
icon={faPlus}
|
||||||
size="md"
|
size='md'
|
||||||
/>
|
/>
|
||||||
{isNew && (
|
{isNew && (
|
||||||
<span className="absolute right-0 flex h-3 w-3 items-center justify-center ml-4 mb-4">
|
<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='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 className='relative inline-flex rounded-full h-3 w-3 bg-primary'></span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center h-full my-48">
|
<div className='flex items-center justify-center h-full my-48'>
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loading.gif"
|
src='/images/loading/loading.gif'
|
||||||
height={60}
|
height={60}
|
||||||
width={100}
|
width={100}
|
||||||
alt="infisical loading indicator"
|
alt='infisical loading indicator'
|
||||||
></Image>
|
></Image>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : data?.length !== 0 ? (
|
||||||
data?.length !== 0 ? (
|
<div className='flex flex-col w-full mt-1 mb-2'>
|
||||||
<div className="flex flex-col w-full mt-1 mb-2">
|
|
||||||
<div
|
<div
|
||||||
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
>
|
>
|
||||||
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
|
<div className='px-1 pt-2 bg-mineshaft-800 rounded-md p-2'>
|
||||||
{!snapshotData && data?.filter(row => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
|
{!snapshotData &&
|
||||||
.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
data
|
||||||
|
?.filter((row) =>
|
||||||
|
row.key
|
||||||
|
?.toUpperCase()
|
||||||
|
.includes(searchKeys.toUpperCase())
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(row) =>
|
||||||
|
!(
|
||||||
|
sharedToHide.includes(row.id) &&
|
||||||
|
row.type == 'shared'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((keyPair) => (
|
||||||
<KeyPair
|
<KeyPair
|
||||||
key={keyPair.id}
|
key={keyPair.id}
|
||||||
keyPair={keyPair}
|
keyPair={keyPair}
|
||||||
@@ -678,16 +856,40 @@ export default function Dashboard() {
|
|||||||
isSnapshot={false}
|
isSnapshot={false}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key))
|
{snapshotData &&
|
||||||
.filter(row => reverseEnvMapping[row.environment] == snapshotEnv)
|
snapshotData.secretVersions
|
||||||
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
?.sort((a, b) => a.key.localeCompare(b.key))
|
||||||
.filter(row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
|
.filter(
|
||||||
|
(row) =>
|
||||||
|
row.environment == selectedSnapshotEnv?.slug
|
||||||
|
)
|
||||||
|
.filter((row) =>
|
||||||
|
row.key
|
||||||
|
.toUpperCase()
|
||||||
|
.includes(searchKeys.toUpperCase())
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(row) =>
|
||||||
|
!(
|
||||||
|
snapshotData.secretVersions
|
||||||
|
?.filter(
|
||||||
|
(row) =>
|
||||||
|
snapshotData.secretVersions
|
||||||
?.map((item) => item.key)
|
?.map((item) => item.key)
|
||||||
.filter(
|
.filter(
|
||||||
(item, index) =>
|
(item, index) =>
|
||||||
index !==
|
index !==
|
||||||
snapshotData.secretVersions?.map((item) => item.key).indexOf(item)
|
snapshotData.secretVersions
|
||||||
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id).includes(row.id) && row.type == 'shared')).map((keyPair) => (
|
?.map((item) => item.key)
|
||||||
|
.indexOf(item)
|
||||||
|
)
|
||||||
|
.includes(row.key) && row.type == 'shared'
|
||||||
|
)
|
||||||
|
?.map((item) => item.id)
|
||||||
|
.includes(row.id) && row.type == 'shared'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((keyPair) => (
|
||||||
<KeyPair
|
<KeyPair
|
||||||
key={keyPair.id}
|
key={keyPair.id}
|
||||||
keyPair={keyPair}
|
keyPair={keyPair}
|
||||||
@@ -703,7 +905,8 @@ export default function Dashboard() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!snapshotData && <div className="w-full max-w-5xl px-2 pt-3">
|
{!snapshotData && (
|
||||||
|
<div className='w-full max-w-5xl px-2 pt-3'>
|
||||||
<DropZone
|
<DropZone
|
||||||
setData={addData}
|
setData={addData}
|
||||||
setErrorDragAndDrop={setErrorDragAndDrop}
|
setErrorDragAndDrop={setErrorDragAndDrop}
|
||||||
@@ -713,11 +916,12 @@ export default function Dashboard() {
|
|||||||
keysExist={true}
|
keysExist={true}
|
||||||
numCurrentRows={data.length}
|
numCurrentRows={data.length}
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
|
<div className='flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28'>
|
||||||
{isKeyAvailable && !snapshotData && (
|
{isKeyAvailable && !snapshotData && (
|
||||||
<DropZone
|
<DropZone
|
||||||
setData={setData}
|
setData={setData}
|
||||||
@@ -729,36 +933,35 @@ export default function Dashboard() {
|
|||||||
keysExist={false}
|
keysExist={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{
|
{!isKeyAvailable && (
|
||||||
(!isKeyAvailable && (
|
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="text-7xl mt-20 mb-8"
|
className='text-7xl mt-20 mb-8'
|
||||||
icon={faFolderOpen}
|
icon={faFolderOpen}
|
||||||
/>
|
/>
|
||||||
<p>
|
<p>
|
||||||
To view this file, contact your administrator for
|
To view this file, contact your administrator for
|
||||||
permission.
|
permission.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1">
|
<p className='mt-1'>
|
||||||
They need to grant you access in the team tab.
|
They need to grant you access in the team tab.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center">
|
<div className='relative z-10 w-10/12 mr-auto h-full ml-2 bg-bunker-800 flex flex-col items-center justify-center'>
|
||||||
<div className="absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full"></div>
|
<div className='absolute top-0 bg-bunker h-14 border-b border-mineshaft-700 w-full'></div>
|
||||||
<Image
|
<Image
|
||||||
src="/images/loading/loading.gif"
|
src='/images/loading/loading.gif'
|
||||||
height={70}
|
height={70}
|
||||||
width={120}
|
width={120}
|
||||||
alt="loading animation"
|
alt='loading animation'
|
||||||
></Image>
|
></Image>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -766,4 +969,4 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
Dashboard.requireAuth = true;
|
Dashboard.requireAuth = true;
|
||||||
|
|
||||||
export const getServerSideProps = getTranslatedServerSideProps(["dashboard"]);
|
export const getServerSideProps = getTranslatedServerSideProps(['dashboard']);
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]);
|
|
||||||
@@ -1,55 +1,87 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import Head from "next/head";
|
import Head from 'next/head';
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import Button from "~/components/basic/buttons/Button";
|
import Button from '~/components/basic/buttons/Button';
|
||||||
import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog";
|
import AddServiceTokenDialog from '~/components/basic/dialog/AddServiceTokenDialog';
|
||||||
import InputField from "~/components/basic/InputField";
|
import InputField from '~/components/basic/InputField';
|
||||||
import EnvironmentTable from '~/components/basic/table/EnvironmentsTable';
|
import EnvironmentTable from '~/components/basic/table/EnvironmentsTable';
|
||||||
import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable";
|
import ServiceTokenTable from '~/components/basic/table/ServiceTokenTable';
|
||||||
import NavHeader from "~/components/navigation/NavHeader";
|
import NavHeader from '~/components/navigation/NavHeader';
|
||||||
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
|
import deleteEnvironment from '~/pages/api/environments/deleteEnvironment';
|
||||||
|
import updateEnvironment from '~/pages/api/environments/updateEnvironment';
|
||||||
|
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
|
||||||
|
|
||||||
import getServiceTokens from "../../api/serviceToken/getServiceTokens";
|
import createEnvironment from '../../api/environments/createEnvironment';
|
||||||
import deleteWorkspace from "../../api/workspace/deleteWorkspace";
|
import getServiceTokens from '../../api/serviceToken/getServiceTokens';
|
||||||
import getWorkspaces from "../../api/workspace/getWorkspaces";
|
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
|
||||||
import renameWorkspace from "../../api/workspace/renameWorkspace";
|
import getWorkspaces from '../../api/workspace/getWorkspaces';
|
||||||
|
import renameWorkspace from '../../api/workspace/renameWorkspace';
|
||||||
|
|
||||||
|
type EnvData = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
export default function SettingsBasic() {
|
export default function SettingsBasic() {
|
||||||
const [buttonReady, setButtonReady] = useState(false);
|
const [buttonReady, setButtonReady] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [workspaceName, setWorkspaceName] = useState("");
|
const [workspaceName, setWorkspaceName] = useState('');
|
||||||
const [serviceTokens, setServiceTokens] = useState([]);
|
const [serviceTokens, setServiceTokens] = useState([]);
|
||||||
const [environments,setEnvironments] = useState([]);
|
const [environments, setEnvironments] = useState<Array<EnvData>>([]);
|
||||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState("");
|
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
|
||||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||||
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] = useState(false);
|
const [isAddServiceTokenDialogOpen, setIsAddServiceTokenDialogOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [projectIdCopied, setProjectIdCopied] = useState(false);
|
||||||
|
const workspaceId = router.query.id as string;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(async () => {
|
/**
|
||||||
|
* 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();
|
const userWorkspaces = await getWorkspaces();
|
||||||
userWorkspaces.forEach((userWorkspace) => {
|
userWorkspaces.forEach((userWorkspace) => {
|
||||||
if (userWorkspace._id == router.query.id) {
|
if (userWorkspace._id == workspaceId) {
|
||||||
setWorkspaceName(userWorkspace.name);
|
setWorkspaceName(userWorkspace.name);
|
||||||
setEnvironments(userWorkspace.environments);
|
setEnvironments(userWorkspace.environments);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const tempServiceTokens = await getServiceTokens({
|
const tempServiceTokens = await getServiceTokens({
|
||||||
workspaceId: router.query.id,
|
workspaceId,
|
||||||
});
|
});
|
||||||
setServiceTokens(tempServiceTokens);
|
setServiceTokens(tempServiceTokens);
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const modifyWorkspaceName = (newName) => {
|
const modifyWorkspaceName = (newName: string) => {
|
||||||
setButtonReady(true);
|
setButtonReady(true);
|
||||||
setWorkspaceName(newName);
|
setWorkspaceName(newName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitChanges = (newWorkspaceName) => {
|
const submitChanges = (newWorkspaceName: string) => {
|
||||||
renameWorkspace(router.query.id, newWorkspaceName);
|
renameWorkspace(workspaceId, newWorkspaceName);
|
||||||
setButtonReady(false);
|
setButtonReady(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,16 +101,54 @@ export default function SettingsBasic() {
|
|||||||
if (userWorkspaces.length > 1) {
|
if (userWorkspaces.length > 1) {
|
||||||
if (
|
if (
|
||||||
userWorkspaces.filter(
|
userWorkspaces.filter(
|
||||||
(workspace) => workspace._id == router.query.id
|
(workspace) => workspace._id === workspaceId
|
||||||
)[0].name == workspaceToBeDeletedName
|
)[0].name == workspaceToBeDeletedName
|
||||||
) {
|
) {
|
||||||
await deleteWorkspace(router.query.id);
|
await deleteWorkspace(workspaceId);
|
||||||
const userWorkspaces = await getWorkspaces();
|
const userWorkspaces = await getWorkspaces();
|
||||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
router.push('/dashboard/' + userWorkspaces[0]._id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCreateEnvironment = async ({ name, slug }: EnvData) => {
|
||||||
|
const res = await createEnvironment(workspaceId, {
|
||||||
|
environmentName: name,
|
||||||
|
environmentSlug: slug,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
// TODO: on react-query migration do an api call to resync
|
||||||
|
setEnvironments((env) => [...env, { name, slug }]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateEnvironment = async (
|
||||||
|
oldSlug: string,
|
||||||
|
{ name, slug }: EnvData
|
||||||
|
) => {
|
||||||
|
const res = await updateEnvironment(workspaceId, {
|
||||||
|
oldEnvironmentSlug: oldSlug,
|
||||||
|
environmentName: name,
|
||||||
|
environmentSlug: slug,
|
||||||
|
});
|
||||||
|
// TODO: on react-query migration do an api call to resync
|
||||||
|
if (res) {
|
||||||
|
setEnvironments((env) =>
|
||||||
|
env.map((el) => (el.slug === oldSlug ? { name, slug } : el))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteEnvironment = async (slugToBeDelete: string) => {
|
||||||
|
const res = await deleteEnvironment(workspaceId, slugToBeDelete);
|
||||||
|
// TODO: on react-query migration do an api call to resync
|
||||||
|
if (res) {
|
||||||
|
setEnvironments((env) =>
|
||||||
|
env.filter(({ slug }) => slug !== slugToBeDelete)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
|
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -89,9 +159,12 @@ export default function SettingsBasic() {
|
|||||||
</Head>
|
</Head>
|
||||||
<AddServiceTokenDialog
|
<AddServiceTokenDialog
|
||||||
isOpen={isAddServiceTokenDialogOpen}
|
isOpen={isAddServiceTokenDialogOpen}
|
||||||
workspaceId={router.query.id}
|
workspaceId={workspaceId}
|
||||||
|
environments={environments}
|
||||||
closeModal={closeAddServiceTokenModal}
|
closeModal={closeAddServiceTokenModal}
|
||||||
workspaceName={workspaceName}
|
workspaceName={workspaceName}
|
||||||
|
serviceTokens={serviceTokens}
|
||||||
|
setServiceTokens={setServiceTokens}
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-row mr-6 max-w-5xl'>
|
<div className='flex flex-row mr-6 max-w-5xl'>
|
||||||
<div className='w-full max-h-screen pb-2 overflow-y-auto'>
|
<div className='w-full max-h-screen pb-2 overflow-y-auto'>
|
||||||
@@ -112,12 +185,13 @@ export default function SettingsBasic() {
|
|||||||
<div className='flex flex-col ml-6 text-mineshaft-50'>
|
<div className='flex flex-col ml-6 text-mineshaft-50'>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<div className='min-w-md mt-2 flex flex-col items-start'>
|
<div className='min-w-md mt-2 flex flex-col items-start'>
|
||||||
<div className='bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start w-full mb-6'>
|
<div className='bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2'>
|
||||||
<p className='text-xl font-semibold mb-4'>
|
<p className='text-xl font-semibold mb-4 mt-2'>
|
||||||
{t('common:display-name')}
|
{t('common:display-name')}
|
||||||
</p>
|
</p>
|
||||||
<div className='max-h-28 w-full max-w-md mr-auto'>
|
<div className='max-h-28 w-full max-w-md mr-auto'>
|
||||||
<InputField
|
<InputField
|
||||||
|
label=''
|
||||||
onChangeHandler={modifyWorkspaceName}
|
onChangeHandler={modifyWorkspaceName}
|
||||||
type='varName'
|
type='varName'
|
||||||
value={workspaceName}
|
value={workspaceName}
|
||||||
@@ -128,7 +202,7 @@ export default function SettingsBasic() {
|
|||||||
<div className='flex justify-start w-full'>
|
<div className='flex justify-start w-full'>
|
||||||
<div className={`flex justify-start max-w-sm mt-4 mb-2`}>
|
<div className={`flex justify-start max-w-sm mt-4 mb-2`}>
|
||||||
<Button
|
<Button
|
||||||
text={t('common:save-changes')}
|
text={t('common:save-changes') as string}
|
||||||
onButtonPressed={() => submitChanges(workspaceName)}
|
onButtonPressed={() => submitChanges(workspaceName)}
|
||||||
color='mineshaft'
|
color='mineshaft'
|
||||||
size='md'
|
size='md'
|
||||||
@@ -139,7 +213,7 @@ export default function SettingsBasic() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start w-full mb-6 mt-4'>
|
<div className='bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start w-full mb-6 mt-4'>
|
||||||
<p className='text-xl font-semibold self-start'>
|
<p className='text-xl font-semibold self-start'>
|
||||||
{t('common:project-id')}
|
{t('common:project-id')}
|
||||||
</p>
|
</p>
|
||||||
@@ -158,30 +232,70 @@ export default function SettingsBasic() {
|
|||||||
{t('settings-project:docs')}
|
{t('settings-project:docs')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<div className='max-h-28 w-ful'>
|
<p className='mt-4 text-xs text-bunker-300'>
|
||||||
<InputField
|
{t('settings-project:auto-generated')}
|
||||||
type='varName'
|
</p>
|
||||||
value={router.query.id}
|
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mb-3 mr-2 rounded-md text-gray-400'>
|
||||||
placeholder=''
|
<p className='mr-2 font-bold pl-4'>{`${t(
|
||||||
isRequired
|
'common:project-id'
|
||||||
static
|
)}:`}</p>
|
||||||
text={t('settings-project:auto-generated')}
|
<input
|
||||||
/>
|
type='text'
|
||||||
|
value={workspaceId}
|
||||||
|
id='myInput'
|
||||||
|
className='bg-white/0 text-gray-400 py-2 w-60 px-2 min-w-md outline-none'
|
||||||
|
disabled
|
||||||
|
></input>
|
||||||
|
<div className='group font-normal group relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||||
|
<button
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className='pl-4 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||||
|
>
|
||||||
|
{projectIdCopied ? (
|
||||||
|
<FontAwesomeIcon icon={faCheck} className='pr-0.5' />
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full pl-3 py-2 bg-bunker-800 rounded-md text-center text-gray-400 text-sm'>
|
||||||
|
{t('common:click-to-copy')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'>
|
<div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'>
|
||||||
|
<EnvironmentTable
|
||||||
|
data={environments}
|
||||||
|
onCreateEnv={onCreateEnvironment}
|
||||||
|
onUpdateEnv={onUpdateEnvironment}
|
||||||
|
onDeleteEnv={onDeleteEnvironment}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-4 mb-4 pt-2'>
|
||||||
<div className='flex flex-row justify-between w-full'>
|
<div className='flex flex-row justify-between w-full'>
|
||||||
<div className='flex flex-col w-full'>
|
<div className='flex flex-col w-full'>
|
||||||
<p className='text-xl font-semibold mb-3'>
|
<p className='text-xl font-semibold mb-3'>
|
||||||
{t('section-token:service-tokens')}
|
{t('section-token:service-tokens')}
|
||||||
</p>
|
</p>
|
||||||
<p className='text-base text-gray-400 mb-4'>
|
<p className='text-sm text-gray-400'>
|
||||||
{t('section-token:service-tokens-description')}
|
{t('section-token:service-tokens-description')}
|
||||||
</p>
|
</p>
|
||||||
|
<p className='text-sm text-gray-400 mb-4'>
|
||||||
|
Please, make sure you are on the
|
||||||
|
<a
|
||||||
|
className='text-primary underline underline-offset-2 ml-1'
|
||||||
|
href='https://infisical.com/docs/cli/overview'
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
latest version of CLI
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-48'>
|
<div className='w-48 mt-2'>
|
||||||
<Button
|
<Button
|
||||||
text={t('section-token:add-new')}
|
text={t('section-token:add-new') as string}
|
||||||
onButtonPressed={() => {
|
onButtonPressed={() => {
|
||||||
setIsAddServiceTokenDialogOpen(true);
|
setIsAddServiceTokenDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
@@ -194,14 +308,12 @@ export default function SettingsBasic() {
|
|||||||
<ServiceTokenTable
|
<ServiceTokenTable
|
||||||
data={serviceTokens}
|
data={serviceTokens}
|
||||||
workspaceName={workspaceName}
|
workspaceName={workspaceName}
|
||||||
|
setServiceTokens={setServiceTokens as any}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start w-full mt-4 mb-4'>
|
|
||||||
<EnvironmentTable data={environments} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='bg-white/5 rounded-md px-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2'>
|
||||||
<div className='bg-white/5 rounded-md px-6 pt-6 pb-6 border-l border-red pl-6 flex flex-col items-start w-full mb-6 mt-4'>
|
|
||||||
<p className='text-xl font-bold text-red'>
|
<p className='text-xl font-bold text-red'>
|
||||||
{t('settings-project:danger-zone')}
|
{t('settings-project:danger-zone')}
|
||||||
</p>
|
</p>
|
||||||
@@ -239,7 +351,7 @@ export default function SettingsBasic() {
|
|||||||
SettingsBasic.requireAuth = true;
|
SettingsBasic.requireAuth = true;
|
||||||
|
|
||||||
export const getServerSideProps = getTranslatedServerSideProps([
|
export const getServerSideProps = getTranslatedServerSideProps([
|
||||||
"settings",
|
'settings',
|
||||||
"settings-project",
|
'settings-project',
|
||||||
"section-token",
|
'section-token',
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user