From 4cce75d1284bc3b0ff276fc0cae4df3d66e4485f Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Sun, 19 Mar 2023 00:37:53 +0530 Subject: [PATCH] feat(ui): implemented the new dashboard page --- frontend/package-lock.json | 442 +++++- frontend/package.json | 2 + frontend/src/pages/_app.tsx | 33 +- frontend/src/pages/dashboard/[id].tsx | 1225 +---------------- frontend/src/styles/globals.css | 38 +- .../src/views/DashboardPage/DashboardPage.tsx | 673 +++++++++ .../DashboardPage/DashboardPage.utils.ts | 231 ++++ .../CompareSecret/CompareSecret.tsx | 64 + .../components/CompareSecret/index.tsx | 1 + .../CreateTagModal/CreateTagModal.tsx | 55 + .../components/CreateTagModal/index.tsx | 1 + .../components/PitDrawer/PitDrawer.tsx | 76 + .../components/PitDrawer/index.tsx | 1 + .../SecretDetailDrawer/GenRandomNumber.tsx | 52 + .../SecretDetailDrawer/SecretDetailDrawer.tsx | 211 +++ .../components/SecretDetailDrawer/index.tsx | 1 + .../SecretDropzone/SecretDropzone.tsx | 130 ++ .../components/SecretDropzone/index.tsx | 1 + .../SecretInputRow/SecretInputRow.tsx | 343 +++++ .../components/SecretInputRow/index.tsx | 1 + .../SecretTableHeader/SecretTableHeader.tsx | 49 + .../components/SecretTableHeader/index.tsx | 1 + frontend/src/views/DashboardPage/index.tsx | 1 + frontend/tailwind.config.js | 19 + 24 files changed, 2405 insertions(+), 1246 deletions(-) create mode 100644 frontend/src/views/DashboardPage/DashboardPage.tsx create mode 100644 frontend/src/views/DashboardPage/DashboardPage.utils.ts create mode 100644 frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx create mode 100644 frontend/src/views/DashboardPage/components/CompareSecret/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx create mode 100644 frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/PitDrawer/PitDrawer.tsx create mode 100644 frontend/src/views/DashboardPage/components/PitDrawer/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretDetailDrawer/GenRandomNumber.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretDetailDrawer/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretDropzone/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretInputRow/index.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretTableHeader/SecretTableHeader.tsx create mode 100644 frontend/src/views/DashboardPage/components/SecretTableHeader/index.tsx create mode 100644 frontend/src/views/DashboardPage/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ad099a70d..384f3a31d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,11 +24,13 @@ "@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-popper": "^1.1.1", "@radix-ui/react-progress": "^1.0.1", "@radix-ui/react-select": "^1.2.0", "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.4", "@reduxjs/toolkit": "^1.8.3", "@stripe/react-stripe-js": "^1.16.3", "@stripe/stripe-js": "^1.46.0", @@ -3310,12 +3312,25 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", - "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", "dependencies": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.1" + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", @@ -3520,6 +3535,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "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", @@ -3575,6 +3625,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.3.tgz", @@ -3602,7 +3687,20 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@radix-ui/react-popper": { + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", @@ -3624,6 +3722,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz", @@ -3732,6 +3865,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", @@ -3807,6 +3975,65 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.4.tgz", + "integrity": "sha512-2h0BvJipRSv7nszzOCdAqb8fH7+Igz24GV1M4aCb/3UPfeMQ+clb1UM5/z2msW3Khir795OCQw2G74YGxjueWw==", + "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-id": "1.0.0", + "@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-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "@radix-ui/react-visually-hidden": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", @@ -24530,12 +24757,23 @@ } }, "@radix-ui/react-arrow": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", - "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.1" + "@radix-ui/react-primitive": "1.0.2" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + } } }, "@radix-ui/react-checkbox": { @@ -24690,6 +24928,35 @@ "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-id": { @@ -24734,6 +25001,35 @@ "@radix-ui/react-use-callback-ref": "1.0.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-popover": { @@ -24757,24 +25053,64 @@ "@radix-ui/react-use-controllable-state": "1.0.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-popper": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", - "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", "requires": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "0.7.2", - "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-arrow": "1.0.2", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0", "@radix-ui/react-use-rect": "1.0.0", "@radix-ui/react-use-size": "1.0.0", "@radix-ui/rect": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + } } }, "@radix-ui/react-portal": { @@ -24859,6 +25195,35 @@ "@radix-ui/react-visually-hidden": "1.0.1", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-slot": { @@ -24921,6 +25286,55 @@ "@radix-ui/react-visually-hidden": "1.0.1" } }, + "@radix-ui/react-tooltip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.4.tgz", + "integrity": "sha512-2h0BvJipRSv7nszzOCdAqb8fH7+Igz24GV1M4aCb/3UPfeMQ+clb1UM5/z2msW3Khir795OCQw2G74YGxjueWw==", + "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-id": "1.0.0", + "@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-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "@radix-ui/react-visually-hidden": "1.0.1" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } + } + }, "@radix-ui/react-use-callback-ref": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d53e40ca7..0e1d3a907 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,11 +31,13 @@ "@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-popper": "^1.1.1", "@radix-ui/react-progress": "^1.0.1", "@radix-ui/react-select": "^1.2.0", "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.4", "@reduxjs/toolkit": "^1.8.3", "@stripe/react-stripe-js": "^1.16.3", "@stripe/stripe-js": "^1.46.0", diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index 2b5dd0fc2..30d2623b5 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -8,6 +8,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import NotificationProvider from '@app/components/context/Notifications/NotificationProvider'; import Telemetry from '@app/components/utilities/telemetry/Telemetry'; +import { TooltipProvider } from '@app/components/v2'; import { publicPaths } from '@app/const'; import { AuthProvider, @@ -73,21 +74,23 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => return ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + ); }; diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx index 799c05c39..4facb61ed 100644 --- a/frontend/src/pages/dashboard/[id].tsx +++ b/frontend/src/pages/dashboard/[id].tsx @@ -1,816 +1,13 @@ -/* eslint-disable no-nested-ternary */ -import { UIEvent, useCallback, useEffect, useRef, useState } from 'react'; import Head from 'next/head'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; -import { - faArrowDown, - faArrowLeft, - faArrowUp, - faCheck, - faClockRotateLeft, - faEye, - faEyeSlash, - faFolderOpen, - faMagnifyingGlass, - faPlus, - faXmark -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tag } from 'public/data/frequentInterfaces'; -import Button from '@app/components/basic/buttons/Button'; -import ListBox from '@app/components/basic/Listbox'; -import BottonRightPopup from '@app/components/basic/popups/BottomRightPopup'; -import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; -import ConfirmEnvOverwriteModal from '@app/components/dashboard/ConfirmEnvOverwriteModal'; -import DownloadSecretMenu from '@app/components/dashboard/DownloadSecretsMenu'; -import DropZone from '@app/components/dashboard/DropZone'; -import KeyPair from '@app/components/dashboard/KeyPair'; -import SideBar from '@app/components/dashboard/SideBar'; -import NavHeader from '@app/components/navigation/NavHeader'; -import { decryptAssymmetric, decryptSymmetric } from '@app/components/utilities/cryptography/crypto'; -import guidGenerator from '@app/components/utilities/randomId'; -import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets'; -import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject'; import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps'; -import { IconButton } from '@app/components/v2'; -import { leaveConfirmDefaultMessage } from '@app/const'; -import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercetSnapshotsCount'; -import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback'; -import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar'; -import { useLeaveConfirm } from '@app/hooks'; +import { DashboardPage } from '@app/views/DashboardPage'; -// import addSecrets from '../api/files/AddSecrets'; -// import deleteSecrets from '../api/files/DeleteSecrets'; -// import updateSecrets from '../api/files/UpdateSecrets'; -import batchSecrets from '../api/files/batchSecrets'; -import getUser from '../api/user/getUser'; -import checkUserAction from '../api/userActions/checkUserAction'; -import registerUserAction from '../api/userActions/registerUserAction'; -import getLatestFileKey from '../api/workspace/getLatestFileKey'; -import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments'; -import getWorkspaces from '../api/workspace/getWorkspaces'; -import getWorkspaceTags from '../api/workspace/getWorkspaceTags'; - -type WorkspaceEnv = { - name: string; - slug: string; - isWriteDenied: boolean; - isReadDenied: boolean; -}; - -interface SecretDataProps { - pos: number; - key: string; - value: string | undefined; - valueOverride: string | undefined; - id: string; - idOverride: string | undefined; - comment: string; - tags: Tag[]; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -interface OverrideProps { - id: string; - keyName: string; - value: string; - pos: number; - comment: string; -} - -interface SnapshotProps { - id: string; - createdAt: string; - version: number; - secretVersions: { - id: string; - pos: number; - environment: string; - key: string; - value: string; - valueOverride: string; - comment: string; - tags: Tag[]; - }[]; -} - -interface EncryptedSecretProps { - _id: string; - createdAt: string; - environment: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - type: 'personal' | 'shared'; - tags: Tag[]; -} - -interface SecretProps { - key: string; - value: string | undefined; - type: 'personal' | 'shared'; - comment: string; - id: string; - tags: Tag[]; -} - -/** - * this function finds the teh duplicates in an array - * @param arr - array of anything (e.g., with secret keys and types (personal/shared)) - * @returns - a list with duplicates - */ -function findDuplicates(arr: any[]) { - const map = new Map(); - return arr.filter((item) => { - if (map.has(item)) { - map.set(item, false); - return true; - } - map.set(item, true); - return false; - }); -} - -/** - * This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets) - * @returns - */ -export default function Dashboard() { - const [data, setData] = useState(); - const [initialData, setInitialData] = useState([]); - const router = useRouter(); - const [blurred, setBlurred] = useState(true); - const [isKeyAvailable, setIsKeyAvailable] = useState(true); - const [isNew, setIsNew] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [searchKeys, setSearchKeys] = useState(''); - const [errorDragAndDrop, setErrorDragAndDrop] = useState(false); - const [sortMethod, setSortMethod] = useState('alphabetical'); - const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); - const [hasUserEverPushed, setHasUserEverPushed] = useState(false); - const [sidebarSecretId, toggleSidebar] = useState('None'); - const [PITSidebarOpen, togglePITSidebar] = useState(false); - const [sharedToHide, setSharedToHide] = useState([]); - const [snapshotData, setSnapshotData] = useState(); - const [numSnapshots, setNumSnapshots] = useState(); - const [saveLoading, setSaveLoading] = useState(false); - const [autoCapitalization, setAutoCapitalization] = useState(false); - const [dropZoneData, setDropZoneData] = useState(); - const [projectTags, setProjectTags] = useState([]); - - const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); +const Dashboard = () => { const { t } = useTranslation(); - const { createNotification } = useNotificationContext(); - - const workspaceId = router.query.id as string; - const [workspaceEnvs, setWorkspaceEnvs] = useState([]); - - const [selectedSnapshotEnv, setSelectedSnapshotEnv] = useState(); - const [selectedEnv, setSelectedEnv] = useState(); - const [atSecretAreaTop, setAtSecretsAreaTop] = useState(true); - const secretsTop = useRef(null); - - const onSecretsAreaScroll = (event: UIEvent) => { - if (event.currentTarget.scrollTop !== 0) { - setAtSecretsAreaTop(false); - } - }; - - // TODO(akhilmhdh): change to FP - const sortValuesHandler = ( - dataToSort: SecretDataProps[] | 1, - specificSortMethod?: 'alphabetical' | '-alphabetical' - ) => { - const howToSort = specificSortMethod === undefined ? sortMethod : specificSortMethod; - const sortedData = (dataToSort !== 1 ? dataToSort : data)! - .sort((a, b) => - howToSort === 'alphabetical' ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key) - ) - .map((item: SecretDataProps, index: number) => ({ - ...item, - pos: index - })); - - setData(sortedData); - setIsLoading(false); - }; - - /** - * Reorder rows alphabetically or in the opprosite order - */ - const reorderRows = (dataToReorder: SecretDataProps[] | 1) => { - if (dataToReorder) { - setSortMethod((prevSort) => (prevSort === 'alphabetical' ? '-alphabetical' : 'alphabetical')); - - sortValuesHandler(dataToReorder, undefined); - } - }; - - useEffect(() => { - (async () => { - if (router.isReady && workspaceId === 'undefined') { - router.push('/noprojects'); - } else try { - const { push, query } = router - const tempNumSnapshots = await getProjectSercetSnapshotsCount({ - workspaceId - }); - setNumSnapshots(tempNumSnapshots); - const userWorkspaces = await getWorkspaces(); - const workspace = userWorkspaces.find((wp) => wp._id === workspaceId); - if (!workspace) { - push(`/dashboard/${userWorkspaces?.[0]?._id}`); - } - setAutoCapitalization(workspace?.autoCapitalization ?? true); - - const accessibleEnvironments = await getWorkspaceEnvironments({ workspaceId }); - setWorkspaceEnvs(accessibleEnvironments || []); - - // set env - const env = accessibleEnvironments?.[0] || { - name: 'unknown', - slug: 'unknown' - }; - setSelectedEnv(env); - setSelectedSnapshotEnv(env); - - if (query.env) { - const index = accessibleEnvironments?.findIndex(({ slug }: { slug: string }) => slug === query.env) - - setSelectedEnv({ - name: accessibleEnvironments?.[index]?.name as string, - slug: query.env as string, - isWriteDenied: accessibleEnvironments?.[index]?.isWriteDenied as boolean, - isReadDenied: accessibleEnvironments?.[index]?.isReadDenied as boolean - }) - setSelectedSnapshotEnv({ - name: accessibleEnvironments?.[index]?.name as string, - slug: query.env as string, - isWriteDenied: accessibleEnvironments?.[index]?.isWriteDenied as boolean, - isReadDenied: accessibleEnvironments?.[index]?.isReadDenied as boolean - }) - - } - - const user = await getUser(); - setIsNew((Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3); - - const userAction = await checkUserAction({ - action: 'first_time_secrets_pushed' - }); - setHasUserEverPushed(!!userAction); - - setProjectTags(await getWorkspaceTags({ workspaceId })); - } catch (error) { - console.log('Error', error); - setData(undefined); - } - })(); - }, [workspaceId]); - - useEffect(() => { - (async () => { - try { - setIsLoading(true); - setBlurred(true); - // ENV - let dataToSort; - if (selectedEnv) { - dataToSort = await getSecretsForProject({ - env: selectedEnv.slug, - setIsKeyAvailable, - setData, - workspaceId - }); - setInitialData(dataToSort); - reorderRows(dataToSort); - } else { - setIsLoading(false); - } - } catch (error) { - console.log('Error', error); - setData(undefined); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedEnv]); - - const addRow = () => { - setIsNew(false); - setData([ - { - id: guidGenerator(), - idOverride: guidGenerator(), - pos: data!.length, - key: '', - value: '', - valueOverride: undefined, - comment: '', - tags: [] - }, - ...data! - ]); - if (!atSecretAreaTop) { - secretsTop.current?.scrollIntoView({ behavior: 'smooth' }); - } - }; - - const addRowToBottom = () => { - setIsNew(false); - setData([ - ...data!, - { - id: guidGenerator(), - idOverride: guidGenerator(), - pos: data!.length, - key: '', - value: '', - valueOverride: undefined, - comment: '', - tags: [] - } - ]); - }; - - const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => { - setHasUnsavedChanges(true); - toggleSidebar('None'); - createNotification({ - text: `${secretName || 'Secret'} has been deleted. Remember to save changes.`, - type: 'error' - }); - sortValuesHandler( - data!.filter((row: SecretDataProps) => !ids.includes(row.id)), - sortMethod === 'alhpabetical' ? '-alphabetical' : 'alphabetical' - ); - }; - - const modifyValue = (value: string, id: string) => { - setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, value } : e))); - setHasUnsavedChanges(true); - }; - - const modifyValueOverride = (valueOverride: string | undefined, id: string) => { - setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, valueOverride } : e))); - setHasUnsavedChanges(true); - }; - - const modifyKey = (key: string, id: string) => { - setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, key } : e))); - setHasUnsavedChanges(true); - }; - - const modifyComment = (comment: string, id: string) => { - setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, comment } : e))); - setHasUnsavedChanges(true); - }; - - const modifyTags = (tags: Tag[], id: string) => { - setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, tags } : e))); - setHasUnsavedChanges(true); - }; - - // For speed purposes and better perforamance, we are using useCallback - const listenChangeValue = useCallback((value: string, id: string) => { - modifyValue(value, id); - }, []); - - const listenChangeValueOverride = useCallback((value: string | undefined, id: string) => { - modifyValueOverride(value, id); - }, []); - - const listenChangeKey = useCallback((value: string, id: string) => { - modifyKey(value, id); - }, []); - - const listenChangeComment = useCallback((value: string, id: string) => { - modifyComment(value, id); - }, []); - - const listenChangeTags = useCallback((value: Tag[], id: string) => { - modifyTags(value, id); - }, []); - - /** - * Save the changes of environment variables and push them to the database - */ - // TODO(akhilmhdh): split and make it small - const savePush = async (dataToPush?: SecretDataProps[]) => { - try { - setSaveLoading(true); - let newData: SecretDataProps[] | null | undefined; - // dataToPush is mostly used for rollbacks, otherwise we always take the current state data - if ((dataToPush ?? [])?.length > 0) { - newData = dataToPush; - } else { - newData = data; - } - - // Checking if any of the secret keys start with a number - if so, don't do anything - const nameErrors = !newData! - .map((secret) => !Number.isNaN(Number(secret.key.charAt(0)))) - .every((v) => v === false); - const emptyNameError = !newData! - .map((secret) => secret.key.length === 0) - .every((v) => v === false); - const duplicatesExist = - findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0; - - if (emptyNameError) { - setSaveLoading(false); - return createNotification({ - text: 'You can`t have empty secret names.', - type: 'error' - }); - } - - if (nameErrors) { - setSaveLoading(false); - return createNotification({ - text: 'Solve all name errors before saving secrets.', - type: 'error' - }); - } - - if (duplicatesExist) { - setSaveLoading(false); - return createNotification({ - text: 'Remove duplicated secret names before saving.', - type: 'error' - }); - } - - if (selectedEnv?.isWriteDenied) { - setSaveLoading(false); - return createNotification({ - text: 'You are not allowed to edit this environment', - type: 'error' - }); - } - - // Once "Save changes" is clicked, disable that button - setHasUnsavedChanges(false); - - const secretsToBeDeleted = initialData! - .filter( - (initDataPoint) => - !newData!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id) - ) - .map((secret) => secret.id); - console.log('delete', secretsToBeDeleted.length); - - const secretsToBeAdded = newData!.filter( - (newDataPoint) => - !initialData!.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id) - ); - console.log('add', secretsToBeAdded.length); - - const secretsToBeUpdated = newData!.filter((newDataPoint) => - initialData! - .filter( - (initDataPoint) => - newData!.filter((dataPoint) => dataPoint.id) - .map((dataPoint) => dataPoint.id).includes(initDataPoint.id) && - (newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !== - initDataPoint.value || - newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !== - initDataPoint.key || - newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !== - initDataPoint.comment || - JSON.stringify(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags) !== - JSON.stringify(initDataPoint?.tags)) - ) - .map((secret) => secret.id) - .includes(newDataPoint.id) - ); - console.log('update', secretsToBeUpdated.length); - - const newOverrides = newData!.filter( - (newDataPoint) => newDataPoint.valueOverride !== undefined - ); - const initOverrides = initialData!.filter( - (initDataPoint) => initDataPoint.valueOverride !== undefined - ); - - const overridesToBeDeleted = initOverrides - .filter( - (initDataPoint) => - !newOverrides!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id) - ) - .map((secret) => String(secret.idOverride)); - console.log('override delete', overridesToBeDeleted.length); - - const overridesToBeAdded = newOverrides! - .filter( - (newDataPoint) => - !initOverrides.map((initDataPoint) => initDataPoint.idOverride).includes(newDataPoint.idOverride) - ) - .map((override) => ({ - pos: override.pos, - key: override.key, - value: String(override.valueOverride), - valueOverride: override.valueOverride, - comment: '', - id: String(override.idOverride), - idOverride: String(override.idOverride), - tags: override.tags - })); - console.log('override add', overridesToBeAdded.length); - - const overridesToBeUpdated = newOverrides! - .filter((newDataPoint) => - initOverrides - .filter( - (initDataPoint) => - newOverrides!.map((dataPoint) => dataPoint.idOverride) - .includes(initDataPoint.idOverride) && - (newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0] - .valueOverride !== initDataPoint.valueOverride || - newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0].key !== - initDataPoint.key || - (newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0] - .comment || '') !== (initDataPoint.comment || '') - || - (JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]?.tags) || '') !== - (JSON.stringify(initDataPoint?.tags) || '') - ) - ) - .map((secret) => secret.idOverride) - .includes(newDataPoint.idOverride) - ) - .map((override) => ({ - pos: override.pos, - key: override.key, - value: String(override.valueOverride), - valueOverride: override.valueOverride, - comment: '', - id: String(override.idOverride), - idOverride: String(override.idOverride), - tags: override.tags - })); - console.log('override update', overridesToBeUpdated.length); - - const requests: any = []; // TODO: fix any - if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) { - secretsToBeDeleted.concat(overridesToBeDeleted).forEach((_id: string) => { - requests.push({ - method: 'DELETE', - secret: { - _id - } - }); - }); - } - if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) { - const secrets = await encryptSecrets({ - secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), - workspaceId, - env: selectedEnv.slug - }); - if (secrets) { - secrets.forEach((secret) => { - requests.push({ - method: 'POST', - secret: { - type: secret.type, - secretKeyCiphertext: secret.secretKeyCiphertext, - secretKeyIV: secret.secretKeyIV, - secretKeyTag: secret.secretKeyTag, - secretValueCiphertext: secret.secretValueCiphertext, - secretValueIV: secret.secretValueIV, - secretValueTag: secret.secretValueTag, - secretCommentCiphertext: secret.secretCommentCiphertext, - secretCommentIV: secret.secretCommentIV, - secretCommentTag: secret.secretCommentTag, - tags: secret.tags - } - }) - }); - } - } - if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) { - const secrets = await encryptSecrets({ - secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), - workspaceId, - env: selectedEnv.slug - }); - if (secrets) { - secrets.forEach((secret) => { - requests.push({ - method: 'PATCH', - secret: { - _id: secret.id, - type: secret.type, - secretKeyCiphertext: secret.secretKeyCiphertext, - secretKeyIV: secret.secretKeyIV, - secretKeyTag: secret.secretKeyTag, - secretValueCiphertext: secret.secretValueCiphertext, - secretValueIV: secret.secretValueIV, - secretValueTag: secret.secretValueTag, - secretCommentCiphertext: secret.secretCommentCiphertext, - secretCommentIV: secret.secretCommentIV, - secretCommentTag: secret.secretCommentTag, - tags: secret.tags - } - }); - }); - } - } - - let newSecrets; - if (selectedEnv && requests.length > 0) { - newSecrets = await batchSecrets({ - workspaceId, - environment: selectedEnv.slug, - requests - }); - } - - let formattedNewDecryptedKeys; - if (newSecrets.createdSecrets) { - const latestKey = await getLatestFileKey({ workspaceId }); - - const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; - - const tempDecryptedSecrets: SecretProps[] = []; - if (latestKey) { - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: latestKey.latestKey.encryptedKey, - nonce: latestKey.latestKey.nonce, - publicKey: latestKey.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - // decrypt secret keys, values, and comments - newSecrets.createdSecrets.forEach((secret: EncryptedSecretProps) => { - const plainTextKey = decryptSymmetric({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key - }); - - let plainTextValue; - if (secret.secretValueCiphertext !== undefined) { - plainTextValue = decryptSymmetric({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); - } else { - plainTextValue = undefined; - } - - let plainTextComment; - if (secret.secretCommentCiphertext) { - plainTextComment = decryptSymmetric({ - ciphertext: secret.secretCommentCiphertext, - iv: secret.secretCommentIV, - tag: secret.secretCommentTag, - key - }); - } else { - plainTextComment = ''; - } - - tempDecryptedSecrets.push({ - id: secret._id, - key: plainTextKey, - value: plainTextValue, - type: secret.type, - comment: plainTextComment, - tags: secret.tags - }); - }); - } - - const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))]; - - formattedNewDecryptedKeys = secretKeys.map((key, index) => ({ - id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0] - ?.id, - idOverride: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === 'personal' - )[0]?.id, - pos: (newData?.filter(dp => !dp.id?.includes('-'))?.length ?? 0) + index, - key, - value: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === 'shared' - )[0]?.value, - valueOverride: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === 'personal' - )[0]?.value, - comment: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === 'shared' - )[0]?.comment, - tags: tempDecryptedSecrets.filter( - (secret) => secret.key === key && secret.type === 'shared' - )[0]?.tags - })); - - setInitialData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id)))); - setData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id)))) - } else { - setInitialData(structuredClone(newData)); - } - - // If this user has never saved environment variables before, show them a prompt to read docs - if (!hasUserEverPushed) { - setCheckDocsPopUpVisible(true); - await registerUserAction({ action: 'first_time_secrets_pushed' }); - } - - // increasing the number of project commits - setNumSnapshots((numSnapshots ?? 0) + 1); - setSaveLoading(false); - createNotification({ - text: `Successfully saved secrets.`, - type: 'success' - }); - } catch (error) { - console.log("Something went wrong while saving secrets: ", error) - createNotification({ - text: `Something went wrong while saving secrets.`, - type: 'error' - }); - } - return undefined; - }; - - const addDataWithMerge = (newData: SecretDataProps[], preserve?: 'old' | 'new') => { - setData((oldData) => { - let filteredOldData = oldData!; - let filteredNewData = newData; - if (preserve === 'new') - filteredOldData = oldData!.filter( - (oldDataPoint) => !newData.find((newDataPoint) => newDataPoint.key === oldDataPoint.key) - ); - if (preserve === 'old') - filteredNewData = newData.filter( - (newDataPoint) => !oldData?.find((oldDataPoint) => oldDataPoint.key === newDataPoint.key) - ); - return filteredOldData.concat(filteredNewData); - }); - setHasUnsavedChanges(true); - }; - - const addData = (newData: SecretDataProps[]) => { - if ( - newData.some((newDataPoint) => data?.find((dataPoint) => dataPoint.key === newDataPoint.key)) // if newData contains duplicates - ) { - setDropZoneData(newData); - return; - } - addDataWithMerge(newData); - }; - - const changeBlurred = () => { - setBlurred(!blurred); - }; - - const deleteCertainRow = ({ ids, secretName }: { ids: string[]; secretName: string }) => { - deleteRow({ ids, secretName }); - }; - - const handleOnEnvironmentChange = (envName: string) => { - if (hasUnsavedChanges) { - if (!window.confirm(leaveConfirmDefaultMessage)) return; - } - - const selectedWorkspaceEnv = workspaceEnvs.find( - ({ name }: { name: string }) => envName === name - ) || { - name: 'unknown', - slug: 'unknown', - isWriteDenied: false, - isReadDenied: false - }; - - if (selectedWorkspaceEnv) { - if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv); - else setSelectedEnv(selectedWorkspaceEnv); - } - - setHasUnsavedChanges(false); - router.push({ - pathname: router.pathname, - query: { ...router.query, env: selectedWorkspaceEnv.slug }, - }) - }; - - return data ? ( -
+ return ( + <> {t('common:head-title', { title: t('dashboard:title') })} @@ -818,417 +15,13 @@ export default function Dashboard() { -
- setDropZoneData(undefined)} - onOverwriteConfirm={(preserve) => { - addDataWithMerge(dropZoneData!, preserve); - setDropZoneData(undefined); - }} - duplicateKeys={ - dropZoneData - ?.filter((newDataPoint) => - data?.find((dataPoint) => dataPoint.key === newDataPoint.key) - ) - .map((duplicate) => duplicate.key) ?? [] - } - /> -
- - {checkDocsPopUpVisible && ( - - )} -
- {snapshotData && ( -
-
- )} -
-
-

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

- {snapshotData && ( - - {new Date(snapshotData.createdAt).toLocaleString()} - - )} - {selectedEnv?.isReadDenied && ( - - Add Only Mode - - )} -
- {!snapshotData && data?.length === 0 && selectedEnv && ( - name)} - onChange={handleOnEnvironmentChange} - /> - )} -
-
-
- {!selectedEnv?.isReadDenied && ( -
- {(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && ( -
-
- )} - {snapshotData && selectedEnv && ( -
-
- )} -
-
-
-
-
- {(snapshotData || data?.length !== 0) && selectedEnv && ( - <> - {!snapshotData ? ( - name)} - onChange={handleOnEnvironmentChange} - /> - ) : ( - name)} - onChange={handleOnEnvironmentChange} - /> - )} -
- - setSearchKeys(e.target.value)} - placeholder={String(t('dashboard:search-keys'))} - /> -
- {!snapshotData && !selectedEnv.isReadDenied && ( -
- -
- )} -
-
- {!snapshotData && ( -
-
- )} - - )} -
-
- {isLoading ? ( -
- infisical loading indicator -
- ) : (data?.length !== 0 || snapshotData?.secretVersions) ? ( -
-
-
-
-
-
-
0
- Key - {!snapshotData && reorderRows(1)} - > - {sortMethod === 'alphabetical' ? : } - } -
-
-
-
- Value -
-
-
-
-
-
- Comment -
-
-
-
-
-
- Tags -
-
-
-
-
-
null} - role="none" - onClick={() => { }} - className="invisible group-hover:visible" - > - -
-
-
-
-
- {!snapshotData && - data - ?.filter((row) => - row.key?.toUpperCase().includes(searchKeys.toUpperCase()) - || row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase()) - || row.comment?.toUpperCase().includes(searchKeys.toUpperCase())) - .filter((row) => !sharedToHide.includes(row.id)) - // .filter((row) => row.value !== undefined) - .map((keyPair) => ( - item.key))?.includes( - keyPair.key - )} - toggleSidebar={toggleSidebar} - togglePITSidebar={togglePITSidebar} - sidebarSecretId={sidebarSecretId} - isSnapshot={false} - deleteRow={deleteCertainRow} - tags={projectTags} - /> - ))} - {snapshotData && - snapshotData.secretVersions - ?.sort((a, b) => a.key.localeCompare(b.key)) - .filter((row) => row.environment === selectedSnapshotEnv?.slug) - .filter((row) => row.key.toUpperCase().includes(searchKeys.toUpperCase())) - .map((keyPair) => ( - item.key))?.includes( - keyPair.key - )} - toggleSidebar={toggleSidebar} - sidebarSecretId={sidebarSecretId} - isSnapshot - tags={projectTags} - /> - ))} -
-
- -
-
- {!snapshotData && ( -
- -
- )} -
-
- ) : ( -
- {isKeyAvailable && !snapshotData && ( - - )} - {!isKeyAvailable && ( - <> - -

To view this file, contact your administrator for permission.

-

They need to grant you access in the team tab.

- - )} -
- )} -
-
- {sidebarSecretId !== 'None' && ( - row.id === sidebarSecretId && row.value !== undefined - )} - modifyKey={listenChangeKey} - modifyValue={listenChangeValue} - modifyValueOverride={listenChangeValueOverride} - modifyComment={listenChangeComment} - buttonReady={hasUnsavedChanges} - workspaceEnvs={workspaceEnvs} - selectedEnv={selectedEnv!} - workspaceId={workspaceId} - savePush={savePush} - sharedToHide={sharedToHide} - setSharedToHide={setSharedToHide} - deleteRow={deleteCertainRow} - /> - )} - {PITSidebarOpen && ( - - )} -
-
- ) : ( -
- loading animation -
+ + ); -} +}; Dashboard.requireAuth = true; export const getServerSideProps = getTranslatedServerSideProps(['dashboard']); + +export default Dashboard; diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index df32da8d0..feca862f6 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -2,6 +2,42 @@ @tailwind components; @tailwind utilities; +@layer utilities { + .flex-0 { + flex:0; + } + .flex-2 { + flex-grow: 2; + } + + .flex-3 { + flex-grow: 3; + } +} + +@layer components { + .secret-table { + @apply bg-mineshaft-800 text-left text-bunker-300 w-full; + } + + /* padding except for comment column */ + .secret-table th { + @apply py-2 px-4 font-medium; + } + + .secret-table td { + @apply py-1 px-1 pr-2 text-sm; + } + + .secret-table th:not(:last-child),.secret-table td:not(:last-child) { + @apply border-r border-mineshaft-600; + } + + .secret-table tr { + @apply border-b border-mineshaft-600; + } +} + @import '@fontsource/inter/400.css'; @import '@fontsource/inter/500.css'; -@import '@fontsource/inter/700.css'; \ No newline at end of file +@import '@fontsource/inter/700.css'; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx new file mode 100644 index 000000000..1aec9f3b8 --- /dev/null +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -0,0 +1,673 @@ +import { useEffect, useState } from 'react'; +import { FormProvider, useFieldArray, useForm, useWatch } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useRouter } from 'next/router'; +import { + faArrowLeft, + faCheck, + faClockRotateLeft, + faCodeCommit, + faDownload, + faEye, + faEyeSlash, + faMagnifyingGlass, + faPlus +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useQueryClient } from '@tanstack/react-query'; + +import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; +import NavHeader from '@app/components/navigation/NavHeader'; +import { + Button, + IconButton, + Input, + Modal, + ModalContent, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectItem, + TableContainer, + Tag, + Tooltip +} from '@app/components/v2'; +import { leaveConfirmDefaultMessage } from '@app/const'; +import { useWorkspace } from '@app/context'; +import { useLeaveConfirm, usePersistentState, usePopUp, useToggle } from '@app/hooks'; +import { + useBatchSecretsOp, + useCreateWsTag, + useGetProjectSecrets, + useGetSecretVersion, + useGetSnapshotSecrets, + useGetUserAction, + useGetUserWsEnvironments, + useGetUserWsKey, + useGetWorkspaceSecretSnapshots, + useGetWsSnapshotCount, + useGetWsTags, + usePerformSecretRollback, + useRegisterUserAction +} from '@app/hooks/api'; +import { secretKeys } from '@app/hooks/api/secrets/queries'; +import { WorkspaceEnv } from '@app/hooks/api/types'; + +import { CompareSecret } from './components/CompareSecret'; +import { CreateTagModal } from './components/CreateTagModal'; +import { PitDrawer } from './components/PitDrawer'; +import { SecretDetailDrawer } from './components/SecretDetailDrawer'; +import { SecretDropzone } from './components/SecretDropzone'; +import { SecretInputRow } from './components/SecretInputRow'; +import { SecretTableHeader } from './components/SecretTableHeader'; +import { + DEFAULT_SECRET_VALUE, + downloadSecret, + FormData, + schema, + transformSecretsToBatchSecretReq, + TSecOverwriteOpt, + TSecretDetailsOpen +} from './DashboardPage.utils'; + +const USER_ACTION_PUSH = 'first_time_secrets_pushed'; + +/* + * Some imp aspects to consider. Here there are multiple stats changing + * Thus ideally we need to use a context. But instead we rely on react hook form + * React hook form provides context and high performance proxy based rendering + * It also handles error handling and transferring states between inputs + * + * Another thing is the purpose of overrideAction + * Before we would remove the value for personal secret when user toggle and user couldn't get it back + * They have to reload the browser or go back all over again + * Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving + * They will get it back + */ +export const DashboardPage = () => { + const { t } = useTranslation(); + const router = useRouter(); + const { createNotification } = useNotificationContext(); + const queryClient = useQueryClient(); + + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + 'secretDetails', + 'addTag', + 'secretSnapshots', + 'uploadedSecOpts', + 'compareSecrets' + ] as const); + const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(); + const [isCommentColumnCollapsed, setIsCommentCollapsed] = usePersistentState( + false, + 'infisical-comment-box' + ); + const [searchFilter, setSearchFilter] = useState(''); + const [snapshotId, setSnaphotId] = useState(null); + const [selectedEnv, setSelectedEnv] = useState(null); + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); + const [deletedSecretIds, setDeletedSecretIds] = useState([]); + const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false }); + + const { currentWorkspace, isLoading } = useWorkspace(); + const workspaceId = currentWorkspace?._id as string; + const { data: latestFileKey } = useGetUserWsKey(workspaceId); + + useEffect(() => { + if (!isLoading && !workspaceId && router.isReady) { + router.push('/noprojects'); + } + }, [isLoading, workspaceId, router.isReady]); + + // fetching data + const { data: userAction } = useGetUserAction(USER_ACTION_PUSH); + const hasUserPushed = Boolean(userAction); + + const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({ + workspaceId, + onSuccess: (data) => { + // get an env with one of the access available + const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied); + if (env) { + setSelectedEnv(env); + } + } + }); + + const { data: secretVersion } = useGetSecretVersion({ + limit: 10, + offset: 0, + secretId: (popUp?.secretDetails?.data as TSecretDetailsOpen)?.id, + decryptFileKey: latestFileKey! + }); + + const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({ + workspaceId, + env: selectedEnv?.slug || '', + decryptFileKey: latestFileKey!, + isPaused: Boolean(snapshotId) + }); + + const { + data: secretSnaphots, + fetchNextPage, + hasNextPage, + isFetchingNextPage + } = useGetWorkspaceSecretSnapshots({ + workspaceId, + limit: 10 + }); + + const { + data: snapshotSecret, + isLoading: isSnapshotSecretsLoading, + isFetching: isSnapshotChanging + } = useGetSnapshotSecrets({ + snapshotId: snapshotId || '', + env: selectedEnv?.slug || '', + decryptFileKey: latestFileKey! + }); + + const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = + useGetWsSnapshotCount(workspaceId); + + const { data: wsTags } = useGetWsTags(workspaceId); + // mutation calls + const { mutateAsync: batchSecretOp } = useBatchSecretsOp(); + const { mutateAsync: performSecretRollback } = usePerformSecretRollback(); + const { mutateAsync: registerUserAction } = useRegisterUserAction(); + const { mutateAsync: createWsTag } = useCreateWsTag(); + + const method = useForm({ + // why any: well yup inferred ts expects other keys to defined as undefined + values: secrets as any, + mode: 'onBlur', + resolver: yupResolver(schema) + }); + + const { + control, + handleSubmit, + getValues, + setValue, + formState: { isDirty, isSubmitting, dirtyFields }, + reset + } = method; + const formSecrets = useWatch({ control, name: 'secrets' }); + const { fields, prepend, append, remove, update } = useFieldArray({ control, name: 'secrets' }); + + const isRollbackMode = Boolean(snapshotId); + const isReadOnly = selectedEnv?.isWriteDenied; + const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied; + const canDoRollback = !isReadOnly && !isAddOnly; + const isSubmitDisabled = + isReadOnly || + // on add only mode the formstate becomes dirty due to secrets missing some items + // to avoid this we check dirtyFields in isAddOnly Mode + (isAddOnly && Object.keys(dirtyFields).length === 0) || + (!isRollbackMode && !isAddOnly && !isDirty) || + isSubmitting; + + useEffect(() => { + if (!isSnapshotChanging && Boolean(snapshotId)) { + reset({ secrets: snapshotSecret?.secrets, isSnapshotMode: true }); + } + }, [isSnapshotChanging]); + + useEffect(() => { + setHasUnsavedChanges(!isSubmitDisabled); + }, [isSubmitDisabled]); + + const onSortSecrets = () => { + const dir = sortDir === 'asc' ? 'desc' : 'asc'; + const sec = getValues('secrets') || []; + const sortedSec = sec.sort((a, b) => + dir === 'asc' ? a?.key?.localeCompare(b?.key || '') : b?.key?.localeCompare(a?.key || '') + ); + setValue('secrets', sortedSec); + setSortDir(dir); + }; + + const handleUploadedEnv = (uploadedSec: TSecOverwriteOpt['secrets']) => { + const sec = getValues('secrets') || []; + const conflictingSec = sec.filter(({ key }) => Boolean(uploadedSec?.[key])); + const conflictingSecIds = conflictingSec.reduce>( + (prev, curr) => ({ + ...prev, + [curr.key]: true + }), + {} + ); + // filter to get all conflicting ones + const conflictingUploadedSec = { ...uploadedSec }; + // append non conflicting ones + Object.keys(uploadedSec).forEach((key) => { + if (!conflictingSecIds?.[key]) { + append({ + ...DEFAULT_SECRET_VALUE, + key, + value: uploadedSec[key].value, + comment: uploadedSec[key].comments.join(',') + }); + delete conflictingUploadedSec[key]; + } + }); + if (conflictingSec.length > 0) { + handlePopUpOpen('uploadedSecOpts', { secrets: conflictingUploadedSec }); + } + }; + + const onOverwriteSecrets = () => { + const sec = getValues('secrets') || []; + const uploadedSec = (popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets; + const data: Array<{ key: string; index: number }> = []; + sec.forEach(({ key }, index) => { + if (uploadedSec?.[key]) data.push({ key, index }); + }); + data.forEach(({ key, index }) => { + const { value, comments } = uploadedSec[key]; + const comment = comments.join(', '); + update(index, { + ...DEFAULT_SECRET_VALUE, + key, + value, + comment, + tags: sec[index].tags + }); + }); + handlePopUpClose('uploadedSecOpts'); + }; + + const onSecretRollback = async () => { + if (!snapshotSecret?.version) { + createNotification({ + text: 'Failed to find secret version', + type: 'success' + }); + return; + } + try { + await performSecretRollback({ + workspaceId, + version: snapshotSecret.version + }); + setValue('isSnapshotMode', false); + setSnaphotId(null); + queryClient.invalidateQueries( + secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || '') + ); + createNotification({ + text: 'Successfully rollback secrets', + type: 'success' + }); + } catch (error) { + console.log(error); + createNotification({ + text: 'Failed to rollback secrets', + type: 'error' + }); + } + }; + + const onAppendSecret = () => append(DEFAULT_SECRET_VALUE); + + const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => { + if (isSnapshotMode) { + await onSecretRollback(); + return; + } + // just closing this if save is triggered from drawer + handlePopUpClose('secretDetails'); + // when add only mode remove rest of things not created + const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec; + // encrypt and format the secrets to batch api format + // requests = [ {method:"", secret:""} ] + const batchedSecret = transformSecretsToBatchSecretReq(deletedSecretIds, latestFileKey, sec); + // type check + if (!selectedEnv?.slug) return; + try { + await batchSecretOp({ + requests: batchedSecret, + workspaceId, + environment: selectedEnv?.slug + }); + createNotification({ + text: 'Successfully saved changes', + type: 'success' + }); + if (!hasUserPushed) { + await registerUserAction(USER_ACTION_PUSH); + } + } catch (error) { + console.log(error); + createNotification({ + text: 'Failed to save changes', + type: 'error' + }); + } + }; + + const onDrawerOpen = (dto: TSecretDetailsOpen) => { + handlePopUpOpen('secretDetails', dto); + }; + + const onEnvChange = (slug: string) => { + if (hasUnsavedChanges) { + // eslint-disable-next-line no-alert + if (!window.confirm(leaveConfirmDefaultMessage)) return; + } + const env = wsEnv?.find((el) => el.slug === slug); + if (env) setSelectedEnv(env); + }; + + // record all deleted ids + // This will make final deletion easier + const onSecretDelete = (index: number, id?: string, overrideId?: string) => { + const ids: string[] = []; + if (id) ids.push(id); + if (overrideId) ids.push(overrideId); + setDeletedSecretIds((state) => [...state, ...ids]); + remove(index); + // just the case if this is called from drawer + handlePopUpClose('secretDetails'); + }; + + const onCreateWsTag = async (tagName: string) => { + try { + await createWsTag({ + workspaceID: workspaceId, + tagName, + tagSlug: tagName.replace(' ', '_') + }); + handlePopUpClose('addTag'); + createNotification({ + text: 'Successfully created a tag', + type: 'success' + }); + } catch (error) { + console.error(error); + createNotification({ + text: 'Failed to create a tag', + type: 'error' + }); + } + }; + + if (isSecretsLoading || isEnvListLoading) { + return ( +
+ loading animation +
+ ); + } + + // when secrets is not loading and secrets list is empty + const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length; + // when using snapshot mode and snapshot is loading and snapshot list is empty + const isSnapshotSecretEmtpy = + isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length; + const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy; + + const userAvailableEnvs = wsEnv?.filter( + ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied + ); + + return ( +
+ +
+ {/* breadcrumb row */} +
+ +
+ {/* Secrets, commit and save button section */} +
+
+

+ {isRollbackMode ? 'Secret Snapshot' : 'Secrets'} +

+ {isRollbackMode && Boolean(snapshotSecret) && ( + + {new Date(snapshotSecret?.createdAt || '').toLocaleString()} + + )} +
+
+ {isRollbackMode && ( + + )} + + +
+
+ {/* Environment, search and other action row */} +
+
+ + + +
+
+ setSearchFilter(e.target.value)} + leftIcon={} + /> +
+
+
+ + + + + + + +
+ +
+
+
+
+ + setIsSecretValueHidden.toggle()} + > + + + + +
+
+
+ {!isSecretEmpty && ( + + + setIsCommentCollapsed(!isCommentColumnCollapsed)} + onSort={onSortSecrets} + /> + + {fields.map(({ id, _id }, index) => ( + onDrawerOpen({ id: _id as string, index })} + isSecretValueHidden={isSecretValueHidden} + isCommentColumnCollapsed={isCommentColumnCollapsed} + wsTags={wsTags} + onCreateTagOpen={() => handlePopUpOpen('addTag')} + /> + ))} + {!isReadOnly && !isRollbackMode && ( + + + + )} + +
+ +
+
+ )} + handlePopUpToggle('secretSnapshots', isOpen)} + fetchNextPage={fetchNextPage} + hasNextPage={hasNextPage} + snapshotId={snapshotId} + isFetchingNextPage={isFetchingNextPage} + secretSnaphots={secretSnaphots} + onSelectSnapshot={setSnaphotId} + /> + handlePopUpToggle('secretDetails', isOpen)} + secretVersion={secretVersion} + index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index} + onEnvCompare={(key) => handlePopUpOpen('compareSecrets', key)} + /> + +
+ {/* secrets table and drawers, modals */} +
+ {/* Create a new tag modal */} + { + handlePopUpToggle('addTag', open); + }} + > + + + + + {/* Uploaded env override or not confirmation modal */} + handlePopUpToggle('uploadedSecOpts', open)} + > + handlePopUpClose('uploadedSecOpts')} + > + Keep old + , + + ]} + > +
+
Your file contains following duplicate secrets
+
+ {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) + ?.map((key) => key) + .join(', ')} +
+
Are you sure you want to overwrite these secrets?
+
+
+
+ handlePopUpToggle('compareSecrets', open)} + > + + + + +
+
+ ); +}; diff --git a/frontend/src/views/DashboardPage/DashboardPage.utils.ts b/frontend/src/views/DashboardPage/DashboardPage.utils.ts new file mode 100644 index 000000000..ef754d1c5 --- /dev/null +++ b/frontend/src/views/DashboardPage/DashboardPage.utils.ts @@ -0,0 +1,231 @@ +import crypto from 'crypto'; + +import * as yup from 'yup'; + +import { + decryptAssymmetric, + encryptSymmetric +} from '@app/components/utilities/cryptography/crypto'; +import { BatchSecretDTO } from '@app/hooks/api/secrets/types'; + +export enum SecretActionType { + Created = 'created', + Modified = 'modified', + Deleted = 'deleted' +} + +export const DEFAULT_SECRET_VALUE = { + _id: undefined, + overrideAction: undefined, + idOverride: undefined, + valueOverride: undefined, + comment: '', + key: '', + value: '', + tags: [] +}; + +const secretSchema = yup.object({ + _id: yup.string(), + key: yup + .string() + .trim() + .required() + .label('Secret key') + .test('starts-with-number', 'Should start with an alphabet', (val) => + Boolean(val?.charAt(0)?.match(/[a-zA-Z]/i)) + ) + .test({ + name: 'duplicate-keys', + // TODO:(akhilmhdh) ts keeps throwing from not found need to see how to resolve this + test: (val, ctx: any) => { + const secrets: Array<{ key: string }> = ctx?.from?.[1]?.value?.secrets || []; + const duplicateKeys: Record = {}; + secrets?.forEach(({ key }, index) => { + if (key === val) duplicateKeys[index + 1] = true; + }); + const pos = Object.keys(duplicateKeys); + if (pos.length <= 1) { + return true; + } + return ctx.createError({ message: `Same key in row ${pos.join(', ')}` }); + } + }), + value: yup.string().trim(), + comment: yup.string().trim(), + tags: yup.array( + yup.object({ + _id: yup.string().required(), + name: yup.string().required(), + slug: yup.string().required() + }) + ), + overrideAction: yup.string().notRequired().oneOf(Object.values(SecretActionType)), + idOverride: yup.string().notRequired(), + valueOverride: yup.string().trim().notRequired() +}); + +export const schema = yup.object({ + isSnapshotMode: yup.bool().notRequired(), + secrets: yup.array(secretSchema) +}); + +export type FormData = yup.InferType; +export type TSecretDetailsOpen = { index: number; id: string }; +export type TSecOverwriteOpt = { secrets: Record }; + +export const downloadSecret = (secrets: FormData['secrets'] = [], env: string = 'unknown') => { + const finalSecret = secrets.map(({ key, value, valueOverride, overrideAction, comment }) => ({ + key, + value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value, + comment + })); + + let file = ''; + finalSecret.forEach(({ key, value, comment }) => { + if (comment) { + file += `# ${comment}\n${key}=${value}\n`; + return; + } + file += `${key}=${value}\n`; + }); + + const blob = new Blob([file]); + const fileDownloadUrl = URL.createObjectURL(blob); + const alink = document.createElement('a'); + alink.href = fileDownloadUrl; + alink.download = `${env}.env`; + alink.click(); +}; + +/* + * Below functions are used convert the dashboard secrets to the bulk secret creation request format + * They are encrypted back + * Formatted to [ { request: "", secret:{} } ] + */ +const encryptASecret = (randomBytes: string, key: string, value?: string, comment?: string) => { + // encrypt key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encryptSymmetric({ + plaintext: key, + key: randomBytes + }); + + // encrypt value + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encryptSymmetric({ + plaintext: value ?? '', + key: randomBytes + }); + + // encrypt comment + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encryptSymmetric({ + plaintext: comment ?? '', + key: randomBytes + }); + + return { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + }; +}; + +export const transformSecretsToBatchSecretReq = ( + deletedSecretIds: string[], + latestFileKey: any, + secrets: FormData['secrets'] +) => { + // deleted secrets + const secretsToBeDeleted: BatchSecretDTO['requests'] = deletedSecretIds.map((id) => ({ + method: 'DELETE', + secret: { _id: id } + })); + + const secretsToBeUpdated: BatchSecretDTO['requests'] = []; + const secretsToBeCreated: BatchSecretDTO['requests'] = []; + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; + + const randomBytes = latestFileKey + ? decryptAssymmetric({ + ciphertext: latestFileKey.encryptedKey, + nonce: latestFileKey.nonce, + publicKey: latestFileKey.sender.publicKey, + privateKey: PRIVATE_KEY + }) + : crypto.randomBytes(16).toString('hex'); + + secrets?.forEach( + ({ _id, idOverride, value, valueOverride, overrideAction, tags = [], comment, key }) => { + if (!idOverride && overrideAction === SecretActionType.Created) { + secretsToBeCreated.push({ + method: 'POST', + secret: { + type: 'personal', + tags, + ...encryptASecret(randomBytes, key, valueOverride, comment) + } + }); + } + // to be created ones as they don't have server generated id + if (!_id) { + secretsToBeCreated.push({ + method: 'POST', + secret: { + type: 'shared', + tags, + ...encryptASecret(randomBytes, key, value, comment) + } + }); + return; // exit as updated and delete case won't happen when created + } + // has an id means this is updated one + if (_id) { + secretsToBeUpdated.push({ + method: 'PATCH', + secret: { + _id, + type: 'shared', + tags, + ...encryptASecret(randomBytes, key, value, comment) + } + }); + } + if (idOverride) { + // if action is deleted meaning override has been removed but id is kept to collect at this point + if (overrideAction === SecretActionType.Deleted) { + secretsToBeDeleted.push({ method: 'DELETE', secret: { _id: idOverride } }); + } else { + // if not deleted action then as id is there its an updated + secretsToBeUpdated.push({ + method: 'PATCH', + secret: { + _id: idOverride, + type: 'personal', + tags, + ...encryptASecret(randomBytes, key, valueOverride, comment) + } + }); + } + } + } + ); + + return secretsToBeCreated.concat(secretsToBeUpdated, secretsToBeDeleted); +}; diff --git a/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx b/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx new file mode 100644 index 000000000..63af91803 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx @@ -0,0 +1,64 @@ +import { useCallback } from 'react'; + +import { FormControl, Input, Spinner } from '@app/components/v2'; +import { useGetProjectSecrets, useGetUserWsKey } from '@app/hooks/api'; + +type SecretValueProps = { + workspaceId: string; + envName: string; + env: string; + secretKey: string; +}; + +const SecretValue = ({ workspaceId, env, envName, secretKey }: SecretValueProps) => { + const { data: latestFileKey } = useGetUserWsKey(workspaceId); + const { data: secret, isLoading: isSecretsLoading } = useGetProjectSecrets({ + workspaceId, + env, + decryptFileKey: latestFileKey! + }); + + const getValue = useCallback( + (data: typeof secret) => { + const sec = data?.secrets?.find(({ key: secKey }) => secKey === secretKey); + return sec?.value || 'Not found'; + }, + [secretKey] + ); + + return ( + + : undefined} + /> + + ); +}; + +type Props = { + workspaceId: string; + secretKey: string; + envs: Array<{ name: string; slug: string }>; +}; + +export const CompareSecret = ({ workspaceId, secretKey, envs }: Props): JSX.Element => { + // should not do anything until secretKey is available + if (!secretKey) return
; + + return ( +
+ {envs.map(({ name, slug }) => ( + + ))} +
+ ); +}; diff --git a/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx b/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx new file mode 100644 index 000000000..871a6529d --- /dev/null +++ b/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx @@ -0,0 +1 @@ +export { CompareSecret } from './CompareSecret'; diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx new file mode 100644 index 000000000..541417cbc --- /dev/null +++ b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx @@ -0,0 +1,55 @@ +import { Controller, useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; + +import { Button, FormControl, Input, ModalClose } from '@app/components/v2'; + +type Props = { + onCreateTag: (tagName: string) => Promise; +}; + +const createTagSchema = yup.object({ + name: yup.string().required().trim().label('Tag Name') +}); +type FormData = yup.InferType; + +export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => { + const { + control, + reset, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: yupResolver(createTagSchema) + }); + + const onFormSubmit = async ({ name }: FormData) => { + await onCreateTag(name); + reset(); + }; + + return ( +
+ ( + + + + )} + /> +
+ + + + +
+ + ); +}; diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx new file mode 100644 index 000000000..a660c64fd --- /dev/null +++ b/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx @@ -0,0 +1 @@ +export {CreateTagModal} from './CreateTagModal' \ No newline at end of file diff --git a/frontend/src/views/DashboardPage/components/PitDrawer/PitDrawer.tsx b/frontend/src/views/DashboardPage/components/PitDrawer/PitDrawer.tsx new file mode 100644 index 000000000..50667ca24 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/PitDrawer/PitDrawer.tsx @@ -0,0 +1,76 @@ +import { Fragment, useCallback } from 'react'; +import { InfiniteData } from '@tanstack/react-query'; + +import { Button, Drawer, DrawerContent } from '@app/components/v2'; +import timeSince from '@app/ee/utilities/timeSince'; +import { TWorkspaceSecretSnapshot } from '@app/hooks/api/secretSnapshots/types'; + +type Props = { + isDrawerOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + secretSnaphots?: InfiniteData; + onSelectSnapshot: (id: string) => void; + snapshotId: string | null; + isFetchingNextPage?: boolean; + hasNextPage?: boolean; + fetchNextPage: () => void; +}; + +export const PitDrawer = ({ + isDrawerOpen, + onOpenChange, + secretSnaphots, + onSelectSnapshot, + snapshotId, + isFetchingNextPage, + hasNextPage, + fetchNextPage +}: Props): JSX.Element => { + const getButtonLabel = useCallback((isFirstChild: boolean, isSelectedSnapshot: boolean) => { + if (isFirstChild) return 'Current Version'; + if (isSelectedSnapshot) return 'Currently Viewing'; + return 'Explore'; + }, []); + + return ( + + +
+ {secretSnaphots?.pages?.map((group, i) => ( + + {group.map(({ _id, createdAt }, index) => ( + + ))} + + ))} +
+ +
+
+ ); +}; diff --git a/frontend/src/views/DashboardPage/components/PitDrawer/index.tsx b/frontend/src/views/DashboardPage/components/PitDrawer/index.tsx new file mode 100644 index 000000000..993b641ff --- /dev/null +++ b/frontend/src/views/DashboardPage/components/PitDrawer/index.tsx @@ -0,0 +1 @@ +export { PitDrawer } from './PitDrawer'; diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/GenRandomNumber.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/GenRandomNumber.tsx new file mode 100644 index 000000000..d64a6ad36 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/GenRandomNumber.tsx @@ -0,0 +1,52 @@ +import crypto from 'crypto'; + +import { useState } from 'react'; +import { useTranslation } from 'next-i18next'; +import { faMinus, faPlus, faShuffle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { Button, Input } from '@app/components/v2'; + +type Props = { + onGenerate: (val: string) => void; +}; + +export const GenRandomNumber = ({ onGenerate }: Props) => { + const { t } = useTranslation(); + const [value, setValue] = useState(32); + + const onGenerateRandomHex = () => { + const rand = crypto.randomBytes(value).toString('hex'); + onGenerate(rand); + }; + + return ( +
+ +
+
+ + setValue(parseInt(e.target.value, 10))} + /> + +
+
+
+ ); +}; diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx new file mode 100644 index 000000000..1fac85361 --- /dev/null +++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx @@ -0,0 +1,211 @@ +import { useFormContext } from 'react-hook-form'; +import { faCircle, faCircleDot, faShuffle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { + Button, + Drawer, + DrawerContent, + FormControl, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Switch, + TextArea +} from '@app/components/v2'; +import { useToggle } from '@app/hooks'; + +import { FormData, SecretActionType } from '../../DashboardPage.utils'; +import { GenRandomNumber } from './GenRandomNumber'; + +type Props = { + isDrawerOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + index: number; + isReadOnly?: boolean; + onEnvCompare: (secretKey: string) => void; + secretVersion?: Array<{ id: string; createdAt: string; value: string }>; + // to record the ids of deleted ones + onSecretDelete: (index: number, id?: string, overrideId?: string) => void; + onSave: () => void; +}; + +export const SecretDetailDrawer = ({ + isDrawerOpen, + onOpenChange, + index, + secretVersion = [], + isReadOnly, + onSecretDelete, + onSave, + onEnvCompare +}: Props): JSX.Element => { + const [canRevealSecVal, setCanRevealSecVal] = useToggle(); + const [canRevealSecOverride, setCanRevealSecOverride] = useToggle(); + + const { register, setValue, watch } = useFormContext(); + const secret = watch(`secrets.${index}`); + + const isOverridden = + secret?.overrideAction === SecretActionType.Created || + secret?.overrideAction === SecretActionType.Modified; + + const onSecretOverride = () => { + if (isOverridden) { + // when user created a new override but then removes + if (SecretActionType.Created) { + setValue(`secrets.${index}.valueOverride`, '', { shouldDirty: true }); + } + setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true }); + } else { + setValue( + `secrets.${index}.overrideAction`, + secret?.idOverride ? SecretActionType.Modified : SecretActionType.Created, + { shouldDirty: true } + ); + } + }; + + if (!secret) { + return
; + } + + return ( + + +
+ +
+
+ + +
+
+ } + > +
+ + + + + + + + + } + /> + + + setValue(`secrets.${index}.value`, val, { shouldDirty: true }) + } + /> + + + +
+ + Override with a personal value + +
+ + + + + + } + /> + + + setValue(`secrets.${index}.valueOverride`, val, { shouldDirty: true }) + } + /> + + + +
+
Version History
+
+ {secretVersion?.map(({ createdAt, value, id }, i) => ( +
+
+
+ +
+
+ {new Date(createdAt).toLocaleDateString('en-US', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + })} +
+
+
+
Value:
+
{value}
+
+
+ ))} +
+
+ +