From 9f9273bb0201d585aa6fde326c22378833294a7b Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 12:54:27 -0800 Subject: [PATCH 1/5] Add tags support for secrets --- backend/src/app.ts | 2 + backend/src/controllers/v2/index.ts | 4 +- .../src/controllers/v2/secretsController.ts | 58 +++++++++------- backend/src/controllers/v2/tagController.ts | 66 +++++++++++++++++++ backend/src/ee/models/secretVersion.ts | 8 ++- backend/src/models/secret.ts | 6 ++ backend/src/models/tag.ts | 49 ++++++++++++++ backend/src/routes/v2/index.ts | 4 +- backend/src/routes/v2/tags.ts | 50 ++++++++++++++ 9 files changed, 221 insertions(+), 26 deletions(-) create mode 100644 backend/src/controllers/v2/tagController.ts create mode 100644 backend/src/models/tag.ts create mode 100644 backend/src/routes/v2/tags.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index 79561d00c..cf44804e9 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -50,6 +50,7 @@ import { serviceTokenData as v2ServiceTokenDataRouter, apiKeyData as v2APIKeyDataRouter, environment as v2EnvironmentRouter, + tags as v2TagsRouter, } from './routes/v2'; import { healthCheck } from './routes/status'; @@ -112,6 +113,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); app.use('/api/v2/users', v2UsersRouter); app.use('/api/v2/organizations', v2OrganizationsRouter); app.use('/api/v2/workspace', v2EnvironmentRouter); +app.use('/api/v2/workspace', v2TagsRouter); app.use('/api/v2/workspace', v2WorkspaceRouter); app.use('/api/v2/secret', v2SecretRouter); // deprecated app.use('/api/v2/secrets', v2SecretsRouter); diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index 936f5e281..3183ac60f 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -6,6 +6,7 @@ import * as apiKeyDataController from './apiKeyDataController'; import * as secretController from './secretController'; import * as secretsController from './secretsController'; import * as environmentController from './environmentController'; +import * as tagController from './tagController'; export { usersController, @@ -15,5 +16,6 @@ export { apiKeyDataController, secretController, secretsController, - environmentController + environmentController, + tagController } diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 5c6f5898e..5ac86e903 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -86,17 +86,28 @@ export const createSecrets = async (req: Request, res: Response) => { throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } - let toAdd; + let listOfSecretsToCreate; if (Array.isArray(req.body.secrets)) { // case: create multiple secrets - toAdd = req.body.secrets; + listOfSecretsToCreate = req.body.secrets; } else if (typeof req.body.secrets === 'object') { // case: create 1 secret - toAdd = [req.body.secrets]; + listOfSecretsToCreate = [req.body.secrets]; } - const newSecrets = await Secret.insertMany( - toAdd.map(({ + type secretsToCreateType = { + type: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + tags: string[] + } + + const newlyCreatedSecrets = await Secret.insertMany( + listOfSecretsToCreate.map(({ type, secretKeyCiphertext, secretKeyIV, @@ -104,15 +115,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - }: { - type: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - }) => { + tags + }: secretsToCreateType) => { return ({ version: 1, workspace: new Types.ObjectId(workspaceId), @@ -124,7 +128,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretKeyTag, secretValueCiphertext, secretValueIV, - secretValueTag + secretValueTag, + tags }); }) ); @@ -140,7 +145,7 @@ export const createSecrets = async (req: Request, res: Response) => { // (EE) add secret versions for new secrets await EESecretService.addSecretVersions({ - secretVersions: newSecrets.map(({ + secretVersions: newlyCreatedSecrets.map(({ _id, version, workspace, @@ -154,7 +159,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash + secretValueHash, + tags }) => ({ _id: new Types.ObjectId(), secret: _id, @@ -171,7 +177,8 @@ export const createSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash + secretValueHash, + tags })) }); @@ -179,7 +186,7 @@ export const createSecrets = async (req: Request, res: Response) => { name: ACTION_ADD_SECRETS, userId: req.user._id, workspaceId: new Types.ObjectId(workspaceId), - secretIds: newSecrets.map((n) => n._id) + secretIds: newlyCreatedSecrets.map((n) => n._id) }); // (EE) create (audit) log @@ -201,7 +208,7 @@ export const createSecrets = async (req: Request, res: Response) => { event: 'secrets added', distinctId: req.user.email, properties: { - numberOfSecrets: toAdd.length, + numberOfSecrets: listOfSecretsToCreate.length, environment, workspaceId, channel: channel, @@ -211,7 +218,7 @@ export const createSecrets = async (req: Request, res: Response) => { } return res.status(200).send({ - secrets: newSecrets + secrets: newlyCreatedSecrets }); } @@ -294,7 +301,7 @@ export const getSecrets = async (req: Request, res: Response) => { ], type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } } - ).then()) + ).populate("tags").then()) if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); @@ -398,6 +405,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: string; secretCommentIV: string; secretCommentTag: string; + tags: string[] } const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => { @@ -410,7 +418,8 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueTag, secretCommentCiphertext, secretCommentIV, - secretCommentTag + secretCommentTag, + tags } = secret; return ({ @@ -426,6 +435,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, + tags, ...(( secretCommentCiphertext && secretCommentIV && @@ -460,6 +470,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext, secretCommentIV, secretCommentTag, + tags } = secretModificationsBySecretId[secret._id.toString()] return ({ @@ -477,6 +488,7 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, + tags: tags ? tags : secret.tags }); }) } diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts new file mode 100644 index 000000000..250ee08a5 --- /dev/null +++ b/backend/src/controllers/v2/tagController.ts @@ -0,0 +1,66 @@ +import { Request, Response } from 'express'; +import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; +import { + Membership, +} from '../../models'; +import Tag, { ITag } from '../../models/tag'; +import { Builder } from "builder-pattern" +import to from 'await-to-js'; +import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors'; +import { MongoError } from 'mongodb'; +import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; + +export const createWorkspaceTag = async (req: Request, res: Response) => { + const { workspaceId } = req.params + const { name, slug } = req.body + const sanitizedTagToCreate = Builder() + .name(name) + .workspace(new Types.ObjectId(workspaceId)) + .slug(slug) + .user(new Types.ObjectId(req.user._id)) + .build(); + + const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate)) + + if (err) { + if ((err as MongoError).code === 11000) { + throw BadRequestError({ message: "Tags must be unique in a workspace" }) + } + + throw err + } + + res.json(createdTag) +} + +export const deleteWorkspaceTag = async (req: Request, res: Response) => { + const { tagId } = req.params + + const tagFromDB = await Tag.findById(tagId) + if (!tagFromDB) { + throw BadRequestError() + } + + // can only delete if the request user is one that belongs to the same workspace as the tag + const membership = await Membership.findOne({ + user: req.user, + workspace: tagFromDB.workspace + }); + + if (!membership) { + UnauthorizedRequestError({ message: 'Failed to validate membership' }); + } + + await Tag.findByIdAndDelete(tagId) + + res.sendStatus(200) +} + +export const getWorkspaceTags = async (req: Request, res: Response) => { + const { workspaceId } = req.params + const workspaceTags = await Tag.find({ workspace: workspaceId }) + return res.json({ + workspaceTags + }) +} \ No newline at end of file diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index 1af4aff2c..efa042765 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -21,6 +21,7 @@ export interface ISecretVersion { secretValueIV: string; secretValueTag: string; secretValueHash: string; + tags?: string[]; } const secretVersionSchema = new Schema( @@ -88,7 +89,12 @@ const secretVersionSchema = new Schema( }, secretValueHash: { type: String - } + }, + tags: { + ref: 'Tag', + type: [Schema.Types.ObjectId], + default: [] + }, }, { timestamps: true diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 6887c8b0f..4ac6c768d 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -23,6 +23,7 @@ export interface ISecret { secretCommentIV?: string; secretCommentTag?: string; secretCommentHash?: string; + tags?: string[]; } const secretSchema = new Schema( @@ -47,6 +48,11 @@ const secretSchema = new Schema( type: Schema.Types.ObjectId, ref: 'User' }, + tags: { + ref: 'Tag', + type: [Schema.Types.ObjectId], + default: [] + }, environment: { type: String, required: true diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts new file mode 100644 index 000000000..6b02c8b1b --- /dev/null +++ b/backend/src/models/tag.ts @@ -0,0 +1,49 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface ITag { + _id: Types.ObjectId; + name: string; + slug: string; + user: Types.ObjectId; + workspace: Types.ObjectId; +} + +const tagSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true, + }, + slug: { + type: String, + required: true, + trim: true, + lowercase: true, + validate: [ + function (value: any) { + return value.indexOf(' ') === -1; + }, + 'slug cannot contain spaces' + ] + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace' + }, + }, + { + timestamps: true + } +); + +tagSchema.index({ slug: 1, workspace: 1 }, { unique: true }) +tagSchema.index({ workspace: 1 }) + +const Tag = model('Tag', tagSchema); + +export default Tag; diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 6f698e316..dfc9ee617 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -6,6 +6,7 @@ import secrets from './secrets'; import serviceTokenData from './serviceTokenData'; import apiKeyData from './apiKeyData'; import environment from "./environment" +import tags from "./tags" export { users, @@ -15,5 +16,6 @@ export { secrets, serviceTokenData, apiKeyData, - environment + environment, + tags } \ No newline at end of file diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts new file mode 100644 index 000000000..d78e1e0f1 --- /dev/null +++ b/backend/src/routes/v2/tags.ts @@ -0,0 +1,50 @@ +import express, { Response, Request } from 'express'; +const router = express.Router(); +import { body, param } from 'express-validator'; +import { tagController } from '../../controllers/v2'; +import { + requireAuth, + requireWorkspaceAuth, + validateRequest, +} from '../../middleware'; +import { ADMIN, MEMBER } from '../../variables'; + +router.get( + '/:workspaceId/tags', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [MEMBER, ADMIN], + }), + param('workspaceId').exists().trim(), + validateRequest, + tagController.getWorkspaceTags +); + +router.delete( + '/tags/:tagId', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + param('tagId').exists().trim(), + validateRequest, + tagController.deleteWorkspaceTag +); + +router.post( + '/:workspaceId/tags', + requireAuth({ + acceptedAuthModes: ['jwt'], + }), + requireWorkspaceAuth({ + acceptedRoles: [MEMBER, ADMIN], + }), + param('workspaceId').exists().trim(), + body('name').exists().trim(), + body('slug').exists().trim(), + validateRequest, + tagController.createWorkspaceTag +); + +export default router; From 31df4a26fac566c20bec3d7f8812ad17fae772c0 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 16:05:34 -0800 Subject: [PATCH 2/5] Update cli docs to be more clear and consistent --- docs/cli/commands/init.mdx | 6 ++-- docs/cli/commands/run.mdx | 61 ++++++++++++++++++++++++++++++----- docs/cli/commands/secrets.mdx | 30 +++-------------- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/docs/cli/commands/init.mdx b/docs/cli/commands/init.mdx index d477541b8..ef33e1d04 100644 --- a/docs/cli/commands/init.mdx +++ b/docs/cli/commands/init.mdx @@ -9,6 +9,8 @@ infisical init ## Description -Link a local project to the platform +Link a local project to your Infisical project. Once connected, you can then access the secrets locally from the connected Infisical project. -The command creates a `infisical.json` file containing your Project ID. + +This command creates a `infisical.json` file containing your Project ID. + diff --git a/docs/cli/commands/run.mdx b/docs/cli/commands/run.mdx index a3f02cf44..fcc9be549 100644 --- a/docs/cli/commands/run.mdx +++ b/docs/cli/commands/run.mdx @@ -25,13 +25,58 @@ description: "The command that injects your secrets into local environment" ## Description -Inject environment variables from the platform into an application process. +Inject secrets from Infisical into your application process. -## Options -| Option | Description | Default value | -| -------------- | ----------------------------------------------------------------------------------------------------------- | ------------- | -| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` | -| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | -| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None | -| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` | +## Subcommands & flags + + + Use this command to inject secrets into your applications process + + ```bash + $ infisical run -- + + # Example + $ infisical run -- npm run dev + ``` + + ### flags + + Pass secrets into multiple commands at once + + ```bash + # Example + infisical run --command="npm run build && npm run dev; more-commands..." + ``` + + + + If you are using a [service token](../../getting-started/dashboard/token) to authenticate, you can pass the token as a flag + + ```bash + # Example + infisical run --token="st.63e03c4a97cb4a747186c71e.ed5b46a34c078a8f94e8228f4ab0ff97.4f7f38034811995997d72badf44b42ec" -- npm run start + ``` + + You may also expose the token to the CLI by setting the environment variable `INFISICAL_TOKEN` before executing the run command. This will have the same effect as setting the token with `--token` flag + + + + Turn on or off the shell parameter expansion in your secrets. If you have used shell parameters in your secret(s), activating this feature will populate them before injecting them into your application process. + + Default value: `true` + + + + This is used to specify the environment from which secrets should be retrieved. The accepted values are the environment slugs defined for your project, such as `dev`, `staging`, `test`, and `prod`. + + Default value: `dev` + + + + Prioritizes personal secrets with the same name over shared secrets + + Default value: `true` + + + diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index 0556e1866..0e4190291 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -14,17 +14,8 @@ This command enables you to perform CRUD (create, read, update, delete) operatio Use this command to print out all of the secrets in your project - ``` + ```bash $ infisical secrets - - ## Example - $ infisical secrets - ┌─────────────┬──────────────┬─────────────┐ - │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ - ├─────────────┼──────────────┼─────────────┤ - │ DOMAIN │ example.com │ shared │ - │ HASH │ jebhfbwe │ shared │ - └─────────────┴──────────────┴─────────────┘ ``` ### flags @@ -45,16 +36,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio This command allows you selectively print the requested secrets by name - ``` + ```bash $ infisical secrets get ... # Example $ infisical secrets get DOMAIN - ┌─────────────┬──────────────┬─────────────┐ - │ SECRET NAME │ SECRET VALUE │ SECRET TYPE │ - ├─────────────┼──────────────┼─────────────┤ - │ DOMAIN │ example.com │ shared │ - └─────────────┴──────────────┴─────────────┘ ``` @@ -70,18 +56,11 @@ This command enables you to perform CRUD (create, read, update, delete) operatio This command allows you to set or update secrets in your environment. If the secret key provided already exists, its value will be updated with the new value. If the secret key does not exist, a new secret will be created using both the key and value provided. -``` +```bash $ infisical secrets set ... ## Example $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe -┌────────────────┬───────────────┬────────────────────────┐ -│ SECRET NAME │ SECRET VALUE │ STATUS │ -├────────────────┼───────────────┼────────────────────────┤ -│ STRIPE_API_KEY │ sjdgwkeudyjwe │ SECRET VALUE UNCHANGED │ -│ DOMAIN │ example.com │ SECRET VALUE MODIFIED │ -│ HASH │ jebhfbwe │ SECRET CREATED │ -└────────────────┴───────────────┴────────────────────────┘ ``` ### Flags @@ -95,12 +74,11 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb This command allows you to delete secrets by their name(s). - ``` + ```bash $ infisical secrets delete ... ## Example $ infisical secrets delete STRIPE_API_KEY DOMAIN HASH - secret name(s) [STRIPE_API_KEY, DOMAIN, HASH] have been deleted from your project ``` ### Flags From c13cb2394273449d21c1e2f82e0fa83647ff4028 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 19:21:07 -0800 Subject: [PATCH 3/5] Add gitlab integ docs --- docs/integrations/cicd/gitlab.mdx | 34 +++++++++++++++++++++++++++++++ docs/mint.json | 1 + 2 files changed, 35 insertions(+) create mode 100644 docs/integrations/cicd/gitlab.mdx diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx new file mode 100644 index 000000000..b84d90e07 --- /dev/null +++ b/docs/integrations/cicd/gitlab.mdx @@ -0,0 +1,34 @@ +--- +title: "Gitlab Pipeline" +--- + +To integrate Infisical secrets into your Gitlab CI/CD setup, three steps are required. + +## Generate service token +To expose Infisical secrets in Gitlab CI/CD, you must generate a service token for the specific project and environment in Infisical. For instructions on how to generate a service token, refer to [this page](../../getting-started/dashboard/token) + +## Set Infisical service token in Gitlab +To provide Infisical CLI with the service token generated in the previous step, go to **Settings > CI/CD > Variables** in Gitlab and create a new **INFISICAL_TOKEN** variable. Enter the generated service token as its value. + +## Configure Infisical in your pipeline +Edit your .gitlab-ci.yml to include the installation of the Infisical CLI. This will allow you to use the CLI for fetching and injecting secrets into any script or command within your Gitlab CI/CD process. + +#### Example +```yaml +image: ubuntu + +stages: + - build + - test + - deploy + +build-job: + stage: build + script: + - apt update && apt install -y curl + - curl -1sLf 'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash + - apt-get update && apt-get install -y infisical + - infisical run -- npm run build + +... +``` \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 441769152..fd334b5ab 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -227,6 +227,7 @@ "group": "CI/CD", "pages": [ "integrations/cicd/githubactions", + "integrations/cicd/gitlab", "integrations/cicd/circleci" ] }, From 56a14925daf8678b1fea9aa6bb1d134fe14566b3 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Sun, 5 Feb 2023 19:23:52 -0800 Subject: [PATCH 4/5] Add githlab to integ overview --- docs/integrations/overview.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 36423e787..eb99ca79d 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -37,7 +37,7 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | GCP | Cloud | Coming soon | | Azure | Cloud | Coming soon | | DigitalOcean | Cloud | Coming soon | -| GitLab | CI/CD | Coming soon | +| [GitLab Pipeline](/integrations/cicd/gitlab) | CI/CD | Available | | [CircleCI](/integrations/cicd/circleci) | CI/CD | Coming soon | | TravisCI | CI/CD | Coming soon | | GitHub Actions | CI/CD | Coming soon | From 086dd621b53c53d2fbbf8f38bf1ba1dfb78cf051 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Sun, 5 Feb 2023 20:29:27 -0800 Subject: [PATCH 5/5] Revamped the dashabord look --- frontend/package-lock.json | 39 +++++ frontend/package.json | 1 + frontend/public/locales/en/common.json | 2 +- frontend/src/components/basic/Listbox.tsx | 2 +- .../components/basic/dialog/DeleteEnvVar.tsx | 2 +- .../dashboard/DashboardInputField.tsx | 126 ++++++++++++---- .../dashboard/DeleteActionButton.tsx | 26 +++- .../dashboard/DownloadSecretsMenu.tsx | 2 +- .../src/components/dashboard/DropZone.tsx | 2 +- frontend/src/components/dashboard/KeyPair.tsx | 77 ++++++---- frontend/src/components/dashboard/SideBar.tsx | 60 ++++---- .../src/components/v2/HoverCard/HoverCard.tsx | 42 ++++++ .../src/components/v2/HoverCard/index.tsx | 2 + .../components/v2/IconButton/IconButton.tsx | 2 +- frontend/src/components/v2/Modal/Modal.tsx | 4 +- .../src/ee/components/PITRecoverySidebar.tsx | 6 +- .../src/ee/components/SecretVersionList.tsx | 2 +- frontend/src/pages/dashboard/[id].tsx | 139 +++++++++++------- 18 files changed, 388 insertions(+), 148 deletions(-) create mode 100644 frontend/src/components/v2/HoverCard/HoverCard.tsx create mode 100644 frontend/src/components/v2/HoverCard/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 283c69b53..7f52e04ae 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dropdown-menu": "^2.0.2", + "@radix-ui/react-hover-card": "^1.0.3", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.3", "@radix-ui/react-progress": "^1.0.1", @@ -3858,6 +3859,27 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.3.tgz", + "integrity": "sha512-rr2+DxPlMhR57IPcNvZ85X8chytdfj7kyVToyR5Ge0r4IJEFiyPs0Cs8/K8oe5zt+yo0F8f29vtC8tNNK+ZIkA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-popper": "1.1.0", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", @@ -25050,6 +25072,23 @@ "@radix-ui/react-use-callback-ref": "1.0.0" } }, + "@radix-ui/react-hover-card": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.3.tgz", + "integrity": "sha512-rr2+DxPlMhR57IPcNvZ85X8chytdfj7kyVToyR5Ge0r4IJEFiyPs0Cs8/K8oe5zt+yo0F8f29vtC8tNNK+ZIkA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-popper": "1.1.0", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0" + } + }, "@radix-ui/react-id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 322e60f31..db8153a0a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dropdown-menu": "^2.0.2", + "@radix-ui/react-hover-card": "^1.0.3", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.3", "@radix-ui/react-progress": "^1.0.1", diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index e02c4f474..3d840c663 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -14,7 +14,7 @@ "save-changes": "Save Changes", "saved": "Saved", "drop-zone": "Drag and drop a .env or .yml file here.", - "drop-zone-keys": "Drag and drop a .env or .yml file here to add more keys.", + "drop-zone-keys": "Drag and drop a .env or .yml file here to add more secrets.", "role": "Role", "role_admin": "admin", "display-name": "Display Name", diff --git a/frontend/src/components/basic/Listbox.tsx b/frontend/src/components/basic/Listbox.tsx index a2f08d9b9..19bdd1dd5 100644 --- a/frontend/src/components/basic/Listbox.tsx +++ b/frontend/src/components/basic/Listbox.tsx @@ -58,7 +58,7 @@ const ListBox = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - + {data.map((person, personIdx) => ( { return (
- {}}> + {}}>
void; value: string | undefined; - type: 'varName' | 'value'; + type: 'varName' | 'value' | 'comment'; blurred?: boolean; isDuplicate?: boolean; - override?: boolean; + overrideEnabled?: boolean; + modifyValueOverride?: (value: string | undefined, position: number) => void; + isSideBarOpen?: boolean; } /** @@ -26,6 +30,8 @@ interface DashboardInputFieldProps { * @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard * @param {boolean} obj.isDuplicate - if the key name is duplicated * @param {boolean} obj.override - whether a secret/row should be displalyed as overriden + * + * * @returns */ @@ -36,7 +42,9 @@ const DashboardInputField = ({ value, blurred, isDuplicate, - override + overrideEnabled, + modifyValueOverride, + isSideBarOpen }: DashboardInputFieldProps) => { const ref = useRef(null); const syncScroll = (e: SyntheticEvent) => { @@ -51,41 +59,97 @@ const DashboardInputField = ({ const error = startsWithNumber || isDuplicate; return ( -
+
onChangeHandler(e.target.value.toUpperCase(), position)} type={type} value={value} - className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${ - error ? 'focus:ring-red/50' : 'focus:ring-primary/50' + className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${ + error ? 'text-red-600 focus:text-red-500' : 'text-bunker-300 focus:text-bunker-100' } duration-200`} spellCheck="false" />
{startsWithNumber && ( -

- Should not start with a number -

+
+ +
)} - {isDuplicate && !startsWithNumber && ( -

- Secret names should be unique -

+ {isDuplicate && value !== '' && !startsWithNumber && ( +
+ +
)} + {!error &&
+ +
} +
+ ); + } + if (type === 'comment') { + const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== ''; + const error = startsWithNumber || isDuplicate; + + return ( +
+
+ onChangeHandler(e.target.value, position)} + type={type} + value={value} + className='z-10 peer font-mono ph-no-capture bg-transparent py-2.5 caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200' + spellCheck="false" + placeholder='–' + /> +
); } if (type === 'value') { return (
-
- {override === true && ( -
+
+ {overrideEnabled === true && ( +
Override enabled
)} @@ -95,19 +159,19 @@ const DashboardInputField = ({ onScroll={syncScroll} className={`${ blurred - ? 'text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent' + ? 'text-transparent focus:text-transparent active:text-transparent' : '' - } z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`} + } z-10 peer font-mono ph-no-capture bg-transparent caret-white text-transparent text-sm px-2 py-2 w-full min-w-16 outline-none duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`} spellCheck="false" />
{value?.split(REGEX).map((word, id) => { if (word.match(REGEX) !== null) { @@ -137,7 +201,9 @@ const DashboardInputField = ({ })}
{blurred && ( -
+
{value?.split('').map(() => ( ))} + {value?.split('').length === 0 && EMPTY}
+
)}
@@ -163,8 +231,8 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && - prev.override === next.override && - prev.isDuplicate === next.isDuplicate + prev.overrideEnabled === next.overrideEnabled && + prev.isDuplicate === next.isDuplicate ); } diff --git a/frontend/src/components/dashboard/DeleteActionButton.tsx b/frontend/src/components/dashboard/DeleteActionButton.tsx index e4d56dd95..d0708f239 100644 --- a/frontend/src/components/dashboard/DeleteActionButton.tsx +++ b/frontend/src/components/dashboard/DeleteActionButton.tsx @@ -1,26 +1,42 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from '../basic/buttons/Button'; import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar'; type Props = { - onSubmit: () => void + onSubmit: () => void; + isPlain?: boolean; } -export const DeleteActionButton = ({ onSubmit }: Props) => { +export const DeleteActionButton = ({ onSubmit, isPlain }: Props) => { const { t } = useTranslation(); const [open, setOpen] = useState(false) return ( -
-
- {!snapshotData && data?.length === 0 && ( + {!snapshotData && data?.length === 0 && selectedEnv && ( name)} @@ -629,7 +646,7 @@ export default function Dashboard() { />
)} - {snapshotData && ( + {snapshotData && selectedEnv && (
-
+
- {(snapshotData || data?.length !== 0) && ( + {(snapshotData || data?.length !== 0) && selectedEnv && ( <> {!snapshotData ? ( )} -
+
setSearchKeys(e.target.value)} placeholder={String(t('dashboard:search-keys'))} />
- {!snapshotData && ( -
-
- )} {!snapshotData && (
@@ -765,13 +773,30 @@ export default function Dashboard() { />
) : data?.length !== 0 ? ( -
+
-
+
+
+
+ Key + {!snapshotData && reorderRows(1)} + > + {sortMethod === 'alphabetical' ? : } + } +
+
Value
+
Comment
+ {!snapshotData &&
} +
+
{!snapshotData && data ?.filter((row) => row.key?.toUpperCase().includes(searchKeys.toUpperCase())) @@ -783,6 +808,7 @@ export default function Dashboard() { modifyValue={listenChangeValue} modifyValueOverride={listenChangeValueOverride} modifyKey={listenChangeKey} + modifyComment={listenChangeComment} isBlurred={blurred} isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( keyPair.key @@ -790,6 +816,7 @@ export default function Dashboard() { toggleSidebar={toggleSidebar} sidebarSecretId={sidebarSecretId} isSnapshot={false} + deleteRow={deleteCertainRow} /> ))} {snapshotData && @@ -820,6 +847,7 @@ export default function Dashboard() { modifyValue={listenChangeValue} modifyValueOverride={listenChangeValueOverride} modifyKey={listenChangeKey} + modifyComment={listenChangeComment} isBlurred={blurred} isDuplicate={findDuplicates(data?.map((item) => item.key))?.includes( keyPair.key @@ -829,9 +857,20 @@ export default function Dashboard() { isSnapshot /> ))} +
+
+ +
{!snapshotData && ( -
+
) : ( -
+
{isKeyAvailable && !snapshotData && (