mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 13:27:22 +00:00
Merge pull request #443 from akhilmhdh/feat/new-dashboard
Feat/new dashboard
This commit is contained in:
Generated
+428
-14
@@ -24,11 +24,13 @@
|
|||||||
"@radix-ui/react-hover-card": "^1.0.3",
|
"@radix-ui/react-hover-card": "^1.0.3",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.3",
|
"@radix-ui/react-popover": "^1.0.3",
|
||||||
|
"@radix-ui/react-popper": "^1.1.1",
|
||||||
"@radix-ui/react-progress": "^1.0.1",
|
"@radix-ui/react-progress": "^1.0.1",
|
||||||
"@radix-ui/react-select": "^1.2.0",
|
"@radix-ui/react-select": "^1.2.0",
|
||||||
"@radix-ui/react-switch": "^1.0.1",
|
"@radix-ui/react-switch": "^1.0.1",
|
||||||
"@radix-ui/react-tabs": "^1.0.2",
|
"@radix-ui/react-tabs": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.2",
|
"@radix-ui/react-toast": "^1.1.2",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.4",
|
||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@stripe/react-stripe-js": "^1.16.3",
|
"@stripe/react-stripe-js": "^1.16.3",
|
||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
@@ -3310,12 +3312,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz",
|
||||||
"integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==",
|
"integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.13.10",
|
"@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": {
|
"peerDependencies": {
|
||||||
"react": "^16.8 || ^17.0 || ^18.0",
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
@@ -3520,6 +3535,41 @@
|
|||||||
"react-dom": "^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-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": {
|
"node_modules/@radix-ui/react-id": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz",
|
"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"
|
"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": {
|
"node_modules/@radix-ui/react-popover": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.3.tgz",
|
"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"
|
"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",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
|
||||||
"integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==",
|
"integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==",
|
||||||
@@ -3624,6 +3722,41 @@
|
|||||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
"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": {
|
"node_modules/@radix-ui/react-portal": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz",
|
"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"
|
"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": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz",
|
"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"
|
"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": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
|
"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": {
|
"@radix-ui/react-arrow": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz",
|
||||||
"integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==",
|
"integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.13.10",
|
"@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": {
|
"@radix-ui/react-checkbox": {
|
||||||
@@ -24690,6 +24928,35 @@
|
|||||||
"@radix-ui/react-presence": "1.0.0",
|
"@radix-ui/react-presence": "1.0.0",
|
||||||
"@radix-ui/react-primitive": "1.0.1",
|
"@radix-ui/react-primitive": "1.0.1",
|
||||||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
"@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": {
|
"@radix-ui/react-id": {
|
||||||
@@ -24734,6 +25001,35 @@
|
|||||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.1.1",
|
||||||
"react-remove-scroll": "2.5.5"
|
"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": {
|
"@radix-ui/react-popover": {
|
||||||
@@ -24757,24 +25053,64 @@
|
|||||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.1.1",
|
||||||
"react-remove-scroll": "2.5.5"
|
"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": {
|
"@radix-ui/react-popper": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz",
|
||||||
"integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==",
|
"integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.13.10",
|
"@babel/runtime": "^7.13.10",
|
||||||
"@floating-ui/react-dom": "0.7.2",
|
"@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-compose-refs": "1.0.0",
|
||||||
"@radix-ui/react-context": "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-callback-ref": "1.0.0",
|
||||||
"@radix-ui/react-use-layout-effect": "1.0.0",
|
"@radix-ui/react-use-layout-effect": "1.0.0",
|
||||||
"@radix-ui/react-use-rect": "1.0.0",
|
"@radix-ui/react-use-rect": "1.0.0",
|
||||||
"@radix-ui/react-use-size": "1.0.0",
|
"@radix-ui/react-use-size": "1.0.0",
|
||||||
"@radix-ui/rect": "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": {
|
"@radix-ui/react-portal": {
|
||||||
@@ -24859,6 +25195,35 @@
|
|||||||
"@radix-ui/react-visually-hidden": "1.0.1",
|
"@radix-ui/react-visually-hidden": "1.0.1",
|
||||||
"aria-hidden": "^1.1.1",
|
"aria-hidden": "^1.1.1",
|
||||||
"react-remove-scroll": "2.5.5"
|
"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": {
|
"@radix-ui/react-slot": {
|
||||||
@@ -24921,6 +25286,55 @@
|
|||||||
"@radix-ui/react-visually-hidden": "1.0.1"
|
"@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": {
|
"@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
|
||||||
|
|||||||
@@ -31,11 +31,13 @@
|
|||||||
"@radix-ui/react-hover-card": "^1.0.3",
|
"@radix-ui/react-hover-card": "^1.0.3",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.3",
|
"@radix-ui/react-popover": "^1.0.3",
|
||||||
|
"@radix-ui/react-popper": "^1.1.1",
|
||||||
"@radix-ui/react-progress": "^1.0.1",
|
"@radix-ui/react-progress": "^1.0.1",
|
||||||
"@radix-ui/react-select": "^1.2.0",
|
"@radix-ui/react-select": "^1.2.0",
|
||||||
"@radix-ui/react-switch": "^1.0.1",
|
"@radix-ui/react-switch": "^1.0.1",
|
||||||
"@radix-ui/react-tabs": "^1.0.2",
|
"@radix-ui/react-tabs": "^1.0.2",
|
||||||
"@radix-ui/react-toast": "^1.1.2",
|
"@radix-ui/react-toast": "^1.1.2",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.4",
|
||||||
"@reduxjs/toolkit": "^1.8.3",
|
"@reduxjs/toolkit": "^1.8.3",
|
||||||
"@stripe/react-stripe-js": "^1.16.3",
|
"@stripe/react-stripe-js": "^1.16.3",
|
||||||
"@stripe/stripe-js": "^1.46.0",
|
"@stripe/stripe-js": "^1.46.0",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type EncryptAsymmetricProps = {
|
|||||||
* @param {Number} obj.time - number of iterations
|
* @param {Number} obj.time - number of iterations
|
||||||
* @param {Number} obj.parallelism - desired parallelism
|
* @param {Number} obj.parallelism - desired parallelism
|
||||||
* @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
|
* @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const deriveArgonKey = async ({
|
const deriveArgonKey = async ({
|
||||||
password,
|
password,
|
||||||
@@ -39,8 +39,8 @@ const deriveArgonKey = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let derivedKey;
|
let derivedKey;
|
||||||
try {
|
try {
|
||||||
derivedKey = await argon2.hash({
|
derivedKey = await argon2.hash({
|
||||||
pass: password,
|
pass: password,
|
||||||
salt,
|
salt,
|
||||||
type: argon2.ArgonType.Argon2id,
|
type: argon2.ArgonType.Argon2id,
|
||||||
mem,
|
mem,
|
||||||
@@ -53,7 +53,7 @@ const deriveArgonKey = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return derivedKey;
|
return derivedKey;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||||
@@ -173,6 +173,7 @@ type DecryptSymmetricProps = {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
|
const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
|
||||||
|
if (!ciphertext) return '';
|
||||||
let plaintext;
|
let plaintext;
|
||||||
try {
|
try {
|
||||||
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
||||||
@@ -184,10 +185,10 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
|
|||||||
return plaintext;
|
return plaintext;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
decryptSymmetric,
|
decryptSymmetric,
|
||||||
deriveArgonKey,
|
deriveArgonKey,
|
||||||
encryptAssymmetric,
|
encryptAssymmetric,
|
||||||
encryptSymmetric
|
encryptSymmetric
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,17 +26,19 @@ const buttonVariants = cva(
|
|||||||
colorSchema: {
|
colorSchema: {
|
||||||
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
|
primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'],
|
||||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
|
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'],
|
||||||
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90']
|
danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90'],
|
||||||
|
gray: ['bg-bunker-500', 'text-bunker-200']
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
solid: '',
|
solid: '',
|
||||||
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
||||||
plain: '',
|
plain: '',
|
||||||
|
selected: '',
|
||||||
// a constant color not in use on hover or click goes colorSchema color
|
// a constant color not in use on hover or click goes colorSchema color
|
||||||
star: 'text-bunker-200 bg-mineshaft-500'
|
star: 'text-bunker-200 bg-mineshaft-500'
|
||||||
},
|
},
|
||||||
isDisabled: {
|
isDisabled: {
|
||||||
true: 'bg-mineshaft opacity-40 cursor-not-allowed',
|
true: 'bg-mineshaft text-white opacity-50 cursor-not-allowed',
|
||||||
false: ''
|
false: ''
|
||||||
},
|
},
|
||||||
isFullWidth: {
|
isFullWidth: {
|
||||||
@@ -60,6 +62,16 @@ const buttonVariants = cva(
|
|||||||
variant: 'star',
|
variant: 'star',
|
||||||
className: 'hover:bg-primary hover:text-black'
|
className: 'hover:bg-primary hover:text-black'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
colorSchema: 'primary',
|
||||||
|
variant: 'selected',
|
||||||
|
className: 'bg-primary/10 border border-primary/50 text-bunker-200'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colorSchema: 'secondary',
|
||||||
|
variant: 'star',
|
||||||
|
className: 'bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
colorSchema: 'danger',
|
colorSchema: 'danger',
|
||||||
variant: 'star',
|
variant: 'star',
|
||||||
@@ -85,6 +97,11 @@ const buttonVariants = cva(
|
|||||||
variant: 'plain',
|
variant: 'plain',
|
||||||
className: 'text-primary'
|
className: 'text-primary'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
colorSchema: 'gray',
|
||||||
|
variant: 'plain',
|
||||||
|
className: 'bg-transparent text-bunker-200'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
colorSchema: 'secondary',
|
colorSchema: 'secondary',
|
||||||
variant: 'plain',
|
variant: 'plain',
|
||||||
@@ -155,26 +172,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
className="absolute rounded-xl"
|
className="absolute rounded-xl"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'shrink-0 cursor-pointer transition-all',
|
'inline-flex shrink-0 cursor-pointer items-center justify-center transition-all',
|
||||||
loadingToggleClass,
|
loadingToggleClass,
|
||||||
leftIcon && 'ml-2',
|
leftIcon && 'ml-2',
|
||||||
size === 'xs' ? 'mr-1' : 'mr-3'
|
size === 'xs' ? 'mr-1' : 'mr-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{leftIcon}
|
{leftIcon}
|
||||||
</span>
|
</div>
|
||||||
<span className={twMerge('transition-all', loadingToggleClass)}>{children}</span>
|
<span className={twMerge('transition-all', isFullWidth ? 'w-full' : 'w-min', loadingToggleClass)}>{children}</span>
|
||||||
<span
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'shrink-0 cursor-pointer transition-all',
|
'inline-flex shrink-0 cursor-pointer items-center justify-center transition-all',
|
||||||
loadingToggleClass,
|
loadingToggleClass,
|
||||||
size === 'xs' ? 'ml-1' : 'ml-3'
|
size === 'xs' ? 'ml-1' : 'ml-2'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{rightIcon}
|
{rightIcon}
|
||||||
</span>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export type CardFooterProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CardFooter = ({ children, className }: CardFooterProps) => (
|
export const CardFooter = ({ children, className }: CardFooterProps) => (
|
||||||
<div className={twMerge('p-6 pt-0', className)}>{children}</div>
|
<div className={twMerge('p-4 pt-0', className)}>{children}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export type CardBodyProps = {
|
export type CardBodyProps = {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Button } from '../Button';
|
||||||
|
import { Drawer, DrawerContent, DrawerContentProps, DrawerTrigger } from './Drawer';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Drawer> = {
|
||||||
|
title: 'Components/Drawer',
|
||||||
|
component: Drawer,
|
||||||
|
tags: ['v2'],
|
||||||
|
argTypes: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof DrawerContent>;
|
||||||
|
|
||||||
|
const Template = (args: DrawerContentProps) => (
|
||||||
|
<Drawer>
|
||||||
|
<DrawerTrigger asChild>
|
||||||
|
<Button>Open</Button>
|
||||||
|
</DrawerTrigger>
|
||||||
|
<DrawerContent {...args}>Hello world</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Basic: Story = {
|
||||||
|
render: (args) => <Template {...args} />,
|
||||||
|
args: {
|
||||||
|
title: 'Title',
|
||||||
|
subTitle: 'Something as subtitle',
|
||||||
|
footerContent: 'footer content'
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { forwardRef, ReactNode } from 'react';
|
||||||
|
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||||
|
import { cva, VariantProps } from 'cva';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { Card, CardBody, CardFooter, CardTitle } from '../Card';
|
||||||
|
import { IconButton } from '../IconButton';
|
||||||
|
|
||||||
|
export type DrawerContentProps = DialogPrimitive.DialogContentProps & {
|
||||||
|
title?: ReactNode;
|
||||||
|
subTitle?: ReactNode;
|
||||||
|
footerContent?: ReactNode;
|
||||||
|
onClose?: () => void;
|
||||||
|
} & VariantProps<typeof drawerContentVariation>;
|
||||||
|
|
||||||
|
const drawerContentVariation = cva(
|
||||||
|
'fixed ease-in-out duration-300 z-[90] border border-mineshaft-600 drop-shadow-2xl',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
direction: {
|
||||||
|
right: [
|
||||||
|
'right-0 top-0',
|
||||||
|
'h-full w-96',
|
||||||
|
'data-[state=open]:animate-drawerRightIn data-[state=closed]:animate-drawerRightOut'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||||
|
(
|
||||||
|
{ children, title, subTitle, className, footerContent, direction = 'right', onClose, ...props },
|
||||||
|
forwardedRef
|
||||||
|
) => (
|
||||||
|
<DialogPrimitive.Portal>
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
className="fixed inset-0 z-[70] h-full w-full"
|
||||||
|
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
|
||||||
|
/>
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
{...props}
|
||||||
|
ref={forwardedRef}
|
||||||
|
className={twMerge(drawerContentVariation({ direction, className }))}
|
||||||
|
>
|
||||||
|
<Card isRounded={false} className="h-full w-full dark">
|
||||||
|
{title && (
|
||||||
|
<CardTitle subTitle={subTitle} className="px-4">
|
||||||
|
{title}
|
||||||
|
</CardTitle>
|
||||||
|
)}
|
||||||
|
<CardBody className="flex-grow overflow-y-auto overflow-x-hidden px-4 dark:[color-scheme:dark]">
|
||||||
|
{children}
|
||||||
|
</CardBody>
|
||||||
|
{footerContent && <CardFooter>{footerContent}</CardFooter>}{' '}
|
||||||
|
<DialogPrimitive.Close aria-label="Close" asChild onClick={onClose}>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="close"
|
||||||
|
className="absolute top-4 right-6 rounded text-bunker-400 hover:text-bunker-50"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||||
|
</IconButton>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</Card>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPrimitive.Portal>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawerContent.displayName = 'ModalContent';
|
||||||
|
|
||||||
|
export type DrawerProps = Omit<DialogPrimitive.DialogProps, 'open'> & { isOpen?: boolean };
|
||||||
|
export const Drawer = ({ isOpen, ...props }: DrawerProps) => (
|
||||||
|
<DialogPrimitive.Root open={isOpen} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DrawerTrigger = DialogPrimitive.Trigger;
|
||||||
|
export type DrawerTriggerProps = DialogPrimitive.DialogTriggerProps;
|
||||||
|
|
||||||
|
export const DrawerClose = DialogPrimitive.Close;
|
||||||
|
export type DrawerCloseProps = DialogPrimitive.DialogCloseProps;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Drawer, DrawerClose,DrawerContent, DrawerTrigger } from './Drawer';
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Button } from '../Button';
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardContentProps,
|
||||||
|
HoverCardTrigger
|
||||||
|
} from './HoverCardv2';
|
||||||
|
|
||||||
|
const meta: Meta<typeof HoverCardContent> = {
|
||||||
|
title: 'Components/HoverCard',
|
||||||
|
component: HoverCard,
|
||||||
|
tags: ['v2']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof HoverCardContent>;
|
||||||
|
|
||||||
|
const Template = (args: HoverCardContentProps) => (
|
||||||
|
<HoverCard>
|
||||||
|
<HoverCardTrigger>
|
||||||
|
<Button>Hello world</Button>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent {...args} sideOffset={10}>
|
||||||
|
<div className="bg-mineshaft-600 p-4 text-gray-400">
|
||||||
|
<h1>This is hover card</h1>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||||
|
export const Primary: Story = {
|
||||||
|
render: (args) => <Template {...args} />
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||||
|
|
||||||
|
export const HoverCard = HoverCardPrimitive.Root;
|
||||||
|
|
||||||
|
export type HoverCardContentProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
} & HoverCardPrimitive.HoverCardContentProps;
|
||||||
|
|
||||||
|
export const HoverCardContent = ({ children, className, ...props }: HoverCardContentProps) => (
|
||||||
|
<HoverCardPrimitive.Portal>
|
||||||
|
<HoverCardPrimitive.Content
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
'relative w-64 rounded-md bg-mineshaft-600 fill-mineshaft-600 p-4 pt-6 font-inter text-gray-200 shadow-md',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<HoverCardPrimitive.Arrow className="fill-inherit stroke-inherit" />
|
||||||
|
</HoverCardPrimitive.Content>
|
||||||
|
</HoverCardPrimitive.Portal>
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { HoverCard,HoverCardContent, HoverCardTrigger } from './HoverCardv2';
|
||||||
@@ -24,12 +24,13 @@ const iconButtonVariants = cva(
|
|||||||
colorSchema: {
|
colorSchema: {
|
||||||
primary: ['bg-primary', 'text-black', 'border-primary hover:opacity-80'],
|
primary: ['bg-primary', 'text-black', 'border-primary hover:opacity-80'],
|
||||||
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-bunker-400'],
|
secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-bunker-400'],
|
||||||
danger: ['bg-[#973939] hover:bg-red', 'text-white', 'border-red']
|
danger: ['bg-[#973939]', 'text-white', 'border-red']
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
solid: '',
|
solid: '',
|
||||||
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
outline: ['bg-transparent', 'border-2', 'border-solid'],
|
||||||
plain: ''
|
plain: '',
|
||||||
|
star: 'text-bunker-200 bg-mineshaft-500'
|
||||||
},
|
},
|
||||||
isDisabled: {
|
isDisabled: {
|
||||||
true: 'bg-opacity-70 cursor-not-allowed',
|
true: 'bg-opacity-70 cursor-not-allowed',
|
||||||
@@ -40,13 +41,23 @@ const iconButtonVariants = cva(
|
|||||||
false: ''
|
false: ''
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
xs: ['text-xs', 'py-2', 'px-2'],
|
xs: ['text-xs', 'rounded-sm', 'py-1.5', 'px-2'],
|
||||||
sm: ['text-sm', 'py-3', 'px-3'],
|
sm: ['text-sm', 'py-3', 'px-3'],
|
||||||
md: ['text-md', 'py-4', 'px-4'],
|
md: ['text-md', 'py-4', 'px-4'],
|
||||||
lg: ['text-lg', 'py-6', 'px-6']
|
lg: ['text-lg', 'py-6', 'px-6']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
compoundVariants: [
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
colorSchema: 'primary',
|
||||||
|
variant: 'star',
|
||||||
|
className: 'hover:bg-primary hover:text-black'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colorSchema: 'danger',
|
||||||
|
variant: 'star',
|
||||||
|
className: 'hover:bg-red hover:text-white'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
colorSchema: 'primary',
|
colorSchema: 'primary',
|
||||||
variant: 'outline',
|
variant: 'outline',
|
||||||
@@ -65,7 +76,7 @@ const iconButtonVariants = cva(
|
|||||||
{
|
{
|
||||||
colorSchema: 'primary',
|
colorSchema: 'primary',
|
||||||
variant: 'plain',
|
variant: 'plain',
|
||||||
className: 'text-primary'
|
className: 'hover:text-primary'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
colorSchema: 'secondary',
|
colorSchema: 'secondary',
|
||||||
@@ -75,7 +86,7 @@ const iconButtonVariants = cva(
|
|||||||
{
|
{
|
||||||
colorSchema: 'danger',
|
colorSchema: 'danger',
|
||||||
variant: 'plain',
|
variant: 'plain',
|
||||||
className: 'text-red'
|
className: 'hover:text-red'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
colorSchema: ['danger', 'primary', 'secondary'],
|
colorSchema: ['danger', 'primary', 'secondary'],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ type Props = {
|
|||||||
leftIcon?: ReactNode;
|
leftIcon?: ReactNode;
|
||||||
rightIcon?: ReactNode;
|
rightIcon?: ReactNode;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
isReadOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
@@ -78,6 +79,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
rightIcon,
|
rightIcon,
|
||||||
variant = 'filled',
|
variant = 'filled',
|
||||||
size = 'md',
|
size = 'md',
|
||||||
|
isReadOnly,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@@ -89,6 +91,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
readOnly={isReadOnly}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
leftIcon ? 'pl-10' : 'pl-2.5',
|
leftIcon ? 'pl-10' : 'pl-2.5',
|
||||||
|
|||||||
@@ -12,20 +12,24 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
|||||||
subTitle?: ReactNode;
|
subTitle?: ReactNode;
|
||||||
footerContent?: ReactNode;
|
footerContent?: ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
overlayClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||||
({ children, title, subTitle, className, footerContent, onClose, ...props }, forwardedRef) => (
|
(
|
||||||
|
{ children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props },
|
||||||
|
forwardedRef
|
||||||
|
) => (
|
||||||
<DialogPrimitive.Portal>
|
<DialogPrimitive.Portal>
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
className="fixed inset-0 h-full w-full animate-fadeIn z-[70]"
|
className={twMerge('fixed inset-0 z-[70] h-full w-full animate-fadeIn', overlayClassName)}
|
||||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
|
style={{ backgroundColor: 'rgba(0, 0, 0, 0.7)' }}
|
||||||
/>
|
/>
|
||||||
<DialogPrimitive.Content {...props} ref={forwardedRef}>
|
<DialogPrimitive.Content {...props} ref={forwardedRef}>
|
||||||
<Card
|
<Card
|
||||||
isRounded
|
isRounded
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'fixed top-1/2 left-1/2 max-w-lg border border-mineshaft-600 -translate-y-2/4 -translate-x-2/4 animate-popIn drop-shadow-2xl z-[90]',
|
'fixed top-1/2 left-1/2 z-[90] max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Button } from '../Button';
|
||||||
|
import { Popover, PopoverContent, PopoverContentProps, PopoverTrigger } from './Popoverv2';
|
||||||
|
|
||||||
|
const meta: Meta<typeof PopoverContent> = {
|
||||||
|
title: 'Components/Popover',
|
||||||
|
component: Popover,
|
||||||
|
tags: ['v2']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof PopoverContent>;
|
||||||
|
|
||||||
|
const Template = (args: PopoverContentProps) => (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button>Hello world</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent {...args} side="bottom" sideOffset={10}>
|
||||||
|
<h1>This is popper</h1>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||||
|
export const Primary: Story = {
|
||||||
|
render: (args) => <Template {...args} />
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { faTimes } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { IconButton } from '../IconButton';
|
||||||
|
|
||||||
|
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||||
|
|
||||||
|
export const Popover = PopoverPrimitive.Root;
|
||||||
|
|
||||||
|
export type PopoverContentProps = {
|
||||||
|
children?: ReactNode;
|
||||||
|
hideCloseBtn?: boolean;
|
||||||
|
} & PopoverPrimitive.PopperContentProps;
|
||||||
|
|
||||||
|
export const PopoverContent = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
hideCloseBtn,
|
||||||
|
...props
|
||||||
|
}: PopoverContentProps) => (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
className={twMerge(
|
||||||
|
[
|
||||||
|
'relative z-[100] w-64 rounded-md bg-mineshaft-600 fill-mineshaft-600 p-4 pt-6 font-inter text-gray-200 shadow-md',
|
||||||
|
// animation
|
||||||
|
'data-[state=open]:data-[side=bottom]:animate-slideUpAndFade',
|
||||||
|
'data-[state=open]:data-[side=top]:animate-slideDownAndFade',
|
||||||
|
'data-[state=open]:data-[side=left]:animate-slideRightAndFade',
|
||||||
|
'data-[state=open]:data-[side=right]:animate-slideLeftAndFade'
|
||||||
|
],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{!hideCloseBtn && (
|
||||||
|
<PopoverPrimitive.Close aria-label="Close" asChild>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="close"
|
||||||
|
className="absolute top-0 right-1 rounded text-bunker-400 hover:text-bunker-50"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTimes} size="lg" className="cursor-pointer" />
|
||||||
|
</IconButton>
|
||||||
|
</PopoverPrimitive.Close>
|
||||||
|
)}
|
||||||
|
<PopoverPrimitive.Arrow className="fill-inherit" />
|
||||||
|
</PopoverPrimitive.Content>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Popover,PopoverContent, PopoverTrigger } from './Popoverv2';
|
||||||
@@ -19,7 +19,7 @@ export const Switch = ({
|
|||||||
isRequired,
|
isRequired,
|
||||||
...props
|
...props
|
||||||
}: SwitchProps): JSX.Element => (
|
}: SwitchProps): JSX.Element => (
|
||||||
<div className="flex items-center text-bunker-300 font-inter">
|
<div className="flex items-center font-inter text-bunker-300">
|
||||||
<label className="text-sm" htmlFor={id}>
|
<label className="text-sm" htmlFor={id}>
|
||||||
{children}
|
{children}
|
||||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||||
@@ -30,13 +30,13 @@ export const Switch = ({
|
|||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'h-5 ml-3 transition-all rounded-full w-9 bg-bunker-300 data-[state=checked]:bg-bunker-200',
|
'ml-3 h-5 w-9 rounded-full bg-bunker-300 transition-all data-[state=checked]:bg-primary',
|
||||||
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
|
isDisabled && 'bg-bunker-400 hover:bg-bunker-400',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
<SwitchPrimitive.Thumb className="w-4 h-4 border-none will-change-transform rounded-full shadow bg-black block transition-all translate-x-0.5 data-[state=checked]:translate-x-[18px]" />
|
<SwitchPrimitive.Thumb className="block h-4 w-4 translate-x-0.5 rounded-full border-none bg-black shadow transition-all will-change-transform data-[state=checked]:translate-x-[18px]" />
|
||||||
</SwitchPrimitive.Root>
|
</SwitchPrimitive.Root>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,24 +1,56 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { faClose } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { cva, VariantProps } from 'cva';
|
import { cva, VariantProps } from 'cva';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
color?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
} & VariantProps<typeof tagVariants>;
|
} & VariantProps<typeof tagVariants>;
|
||||||
|
|
||||||
const tagVariants = cva('inline-flex whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 ', {
|
const tagVariants = cva(
|
||||||
variants: {
|
'inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200',
|
||||||
colorSchema: {
|
{
|
||||||
gray: 'bg-mineshaft-500',
|
variants: {
|
||||||
red: 'bg-red/80 text-bunker-100'
|
colorSchema: {
|
||||||
},
|
gray: 'bg-mineshaft-500',
|
||||||
size: {
|
red: 'bg-red/80 text-bunker-100',
|
||||||
sm: 'px-1.5 py-0.5'
|
green: 'bg-primary-800 text-white'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: 'px-2 py-0.5'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
export const Tag = ({ children, className, colorSchema = 'gray', size = 'sm' }: Props) => (
|
export const Tag = ({
|
||||||
<div className={twMerge(tagVariants({ colorSchema, className, size }))}>{children}</div>
|
children,
|
||||||
|
className,
|
||||||
|
colorSchema = 'gray',
|
||||||
|
color,
|
||||||
|
isDisabled,
|
||||||
|
size = 'sm',
|
||||||
|
onClose
|
||||||
|
}: Props) => (
|
||||||
|
<div
|
||||||
|
className={twMerge(tagVariants({ colorSchema, className, size }))}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{onClose && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isDisabled}
|
||||||
|
className="ml-2 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faClose} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { cva, VariantProps } from 'cva';
|
|||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isDisabled?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isFullWidth?: boolean;
|
isFullWidth?: boolean;
|
||||||
isRequired?: boolean;
|
isRequired?: boolean;
|
||||||
@@ -10,7 +11,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const textAreaVariants = cva(
|
const textAreaVariants = cva(
|
||||||
'textarea w-full p-2 border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50',
|
'textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
@@ -43,7 +44,7 @@ const textAreaVariants = cva(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'> &
|
export type TextAreaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'size' | 'disabled'> &
|
||||||
VariantProps<typeof textAreaVariants> &
|
VariantProps<typeof textAreaVariants> &
|
||||||
Props;
|
Props;
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
isRounded = true,
|
isRounded = true,
|
||||||
|
isDisabled = false,
|
||||||
isError = false,
|
isError = false,
|
||||||
isRequired,
|
isRequired,
|
||||||
variant = 'filled',
|
variant = 'filled',
|
||||||
@@ -66,6 +68,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
style={{ resize: reSize }}
|
style={{ resize: reSize }}
|
||||||
required={isRequired}
|
required={isRequired}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
disabled={isDisabled}
|
||||||
className={twMerge(textAreaVariants({ className, isError, size, isRounded, variant }))}
|
className={twMerge(textAreaVariants({ className, isError, size, isRounded, variant }))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Tooltip, TooltipProps, TooltipProvider } from './Tooltip';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Tooltip> = {
|
||||||
|
title: 'Components/Tooltip',
|
||||||
|
component: Tooltip,
|
||||||
|
tags: ['v2'],
|
||||||
|
args: {
|
||||||
|
content: 'Hi'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Tooltip>;
|
||||||
|
|
||||||
|
const Template = (args: TooltipProps) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip {...args}>
|
||||||
|
<div className="text-white">Hello infisical</div>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||||
|
export const Primary: Story = {
|
||||||
|
render: (args) => <Template {...args} />
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export type TooltipProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
content?: ReactNode;
|
||||||
|
isOpen?: boolean;
|
||||||
|
className?: string;
|
||||||
|
asChild?: boolean;
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
} & Omit<TooltipPrimitive.TooltipContentProps, 'open'>;
|
||||||
|
|
||||||
|
export const Tooltip = ({
|
||||||
|
children,
|
||||||
|
content,
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
defaultOpen,
|
||||||
|
className,
|
||||||
|
asChild = true,
|
||||||
|
...props
|
||||||
|
}: TooltipProps) => (
|
||||||
|
<TooltipPrimitive.Root
|
||||||
|
delayDuration={150}
|
||||||
|
open={isOpen}
|
||||||
|
defaultOpen={defaultOpen}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
>
|
||||||
|
<TooltipPrimitive.Trigger asChild={asChild}>{children}</TooltipPrimitive.Trigger>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
side="top"
|
||||||
|
align="center"
|
||||||
|
sideOffset={5}
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
`z-20 select-none rounded-md bg-mineshaft-500 py-2 px-4 text-sm text-bunker-200 shadow-md
|
||||||
|
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
|
||||||
|
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
||||||
|
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
||||||
|
data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade
|
||||||
|
`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
<TooltipPrimitive.Arrow width={11} height={5} className="fill-mineshaft-500" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TooltipProvider = TooltipPrimitive.Provider;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Tooltip, TooltipProvider } from './Tooltip';
|
||||||
@@ -2,14 +2,17 @@ export * from './Button';
|
|||||||
export * from './Card';
|
export * from './Card';
|
||||||
export * from './Checkbox';
|
export * from './Checkbox';
|
||||||
export * from './DeleteActionModal';
|
export * from './DeleteActionModal';
|
||||||
|
export * from './Drawer';
|
||||||
export * from './Dropdown';
|
export * from './Dropdown';
|
||||||
export * from './EmailServiceSetupModal'
|
export * from './EmailServiceSetupModal'
|
||||||
export * from './EmptyState';
|
export * from './EmptyState';
|
||||||
export * from './FormControl';
|
export * from './FormControl';
|
||||||
|
export * from './HoverCardv2';
|
||||||
export * from './IconButton';
|
export * from './IconButton';
|
||||||
export * from './Input';
|
export * from './Input';
|
||||||
export * from './Menu';
|
export * from './Menu';
|
||||||
export * from './Modal';
|
export * from './Modal';
|
||||||
|
export * from './Popoverv2';
|
||||||
export * from './Select';
|
export * from './Select';
|
||||||
export * from './Skeleton';
|
export * from './Skeleton';
|
||||||
export * from './Spinner';
|
export * from './Spinner';
|
||||||
@@ -17,4 +20,5 @@ export * from './Switch';
|
|||||||
export * from './Table';
|
export * from './Table';
|
||||||
export * from './Tag';
|
export * from './Tag';
|
||||||
export * from './TextArea';
|
export * from './TextArea';
|
||||||
|
export * from './Tooltip';
|
||||||
export * from './UpgradePlanModal';
|
export * from './UpgradePlanModal';
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export * from './auth';
|
|||||||
export * from './incidentContacts';
|
export * from './incidentContacts';
|
||||||
export * from './keys';
|
export * from './keys';
|
||||||
export * from './organization';
|
export * from './organization';
|
||||||
|
export * from './secrets';
|
||||||
|
export * from './secretSnapshots';
|
||||||
export * from './serviceTokens';
|
export * from './serviceTokens';
|
||||||
export * from './subscriptions';
|
export * from './subscriptions';
|
||||||
export * from './tags';
|
export * from './tags';
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export {
|
||||||
|
useGetSnapshotSecrets,
|
||||||
|
useGetWorkspaceSecretSnapshots,
|
||||||
|
useGetWsSnapshotCount,
|
||||||
|
usePerformSecretRollback
|
||||||
|
} from './queries';
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
decryptSymmetric
|
||||||
|
} from '@app/components/utilities/cryptography/crypto';
|
||||||
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
|
import { DecryptedSecret } from '../secrets/types';
|
||||||
|
import {
|
||||||
|
GetWorkspaceSecretSnapshotsDTO,
|
||||||
|
TSecretRollbackDTO,
|
||||||
|
TSnapshotSecret,
|
||||||
|
TSnapshotSecretProps,
|
||||||
|
TWorkspaceSecretSnapshot
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const secretSnapshotKeys = {
|
||||||
|
list: (workspaceId: string) => [{ workspaceId }, 'secret-snapshot'] as const,
|
||||||
|
snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const,
|
||||||
|
count: (workspaceId: string) => [{ workspaceId }, 'count', 'secret-snapshot']
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, offset = 0) => {
|
||||||
|
const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>(
|
||||||
|
`/api/v1/workspace/${workspaceId}/secret-snapshots`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.data.secretSnapshots;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) =>
|
||||||
|
useInfiniteQuery({
|
||||||
|
enabled: Boolean(dto.workspaceId),
|
||||||
|
queryKey: secretSnapshotKeys.list(dto.workspaceId),
|
||||||
|
queryFn: ({ pageParam }) => fetchWorkspaceSecretSnaphots(dto.workspaceId, dto.limit, pageParam),
|
||||||
|
getNextPageParam: (lastPage, pages) =>
|
||||||
|
lastPage.length !== 0 ? pages.length * dto.limit : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchSnapshotEncSecrets = async (snapshotId: string) => {
|
||||||
|
const res = await apiRequest.get<{ secretSnapshot: TSnapshotSecret }>(
|
||||||
|
`/api/v1/secret-snapshot/${snapshotId}`
|
||||||
|
);
|
||||||
|
return res.data.secretSnapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnapshotSecretProps) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: secretSnapshotKeys.snapshotSecrets(snapshotId),
|
||||||
|
enabled: Boolean(snapshotId && decryptFileKey),
|
||||||
|
queryFn: () => fetchSnapshotEncSecrets(snapshotId),
|
||||||
|
select: (data) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
|
const latestKey = decryptFileKey;
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const sharedSecrets: DecryptedSecret[] = [];
|
||||||
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
|
data.secretVersions
|
||||||
|
.filter(({ environment }) => environment === env)
|
||||||
|
.forEach((encSecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
|
iv: encSecret.secretValueIV,
|
||||||
|
tag: encSecret.secretValueTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretComment = '';
|
||||||
|
|
||||||
|
const decryptedSecret = {
|
||||||
|
_id: encSecret.secret,
|
||||||
|
env: encSecret.environment,
|
||||||
|
key: secretKey,
|
||||||
|
value: secretValue,
|
||||||
|
tags: encSecret.tags,
|
||||||
|
comment: secretComment,
|
||||||
|
createdAt: encSecret.createdAt,
|
||||||
|
updatedAt: encSecret.updatedAt,
|
||||||
|
type: 'modified'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encSecret.type === 'personal') {
|
||||||
|
personalSecrets[decryptedSecret.key] = { id: encSecret.secret, value: secretValue };
|
||||||
|
} else {
|
||||||
|
sharedSecrets.push(decryptedSecret);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sharedSecrets.forEach((val) => {
|
||||||
|
if (personalSecrets?.[val.key]) {
|
||||||
|
val.idOverride = personalSecrets[val.key].id;
|
||||||
|
val.valueOverride = personalSecrets[val.key].value;
|
||||||
|
val.overrideAction = 'modified';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { version: data.version, secrets: sharedSecrets, createdAt: data.createdAt };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchWorkspaceSecretSnaphotCount = async (workspaceId: string) => {
|
||||||
|
const res = await apiRequest.get<{ count: number }>(
|
||||||
|
`/api/v1/workspace/${workspaceId}/secret-snapshots/count`
|
||||||
|
);
|
||||||
|
return res.data.count;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetWsSnapshotCount = (workspaceId: string) =>
|
||||||
|
useQuery({
|
||||||
|
enabled: Boolean(workspaceId),
|
||||||
|
queryKey: secretSnapshotKeys.count(workspaceId),
|
||||||
|
queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usePerformSecretRollback = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, TSecretRollbackDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.post(
|
||||||
|
`/api/v1/workspace/${dto.workspaceId}/secret-snapshots/rollback`,
|
||||||
|
{
|
||||||
|
version: dto.version
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, dto) => {
|
||||||
|
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
|
||||||
|
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { UserWsKeyPair } from '../keys/types';
|
||||||
|
import { EncryptedSecretVersion } from '../secrets/types';
|
||||||
|
|
||||||
|
export type TWorkspaceSecretSnapshot = {
|
||||||
|
_id: string;
|
||||||
|
workspace: string;
|
||||||
|
version: number;
|
||||||
|
secretVersions: string[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
__v: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSnapshotSecret = Omit<TWorkspaceSecretSnapshot, 'secretVersions'> & {
|
||||||
|
secretVersions: EncryptedSecretVersion[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSnapshotSecretProps = {
|
||||||
|
snapshotId: string;
|
||||||
|
env: string;
|
||||||
|
decryptFileKey: UserWsKeyPair;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetWorkspaceSecretSnapshotsDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretRollbackDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useBatchSecretsOp, useGetProjectSecrets, useGetSecretVersion } from './queries';
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
decryptSymmetric
|
||||||
|
} from '@app/components/utilities/cryptography/crypto';
|
||||||
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
|
import { secretSnapshotKeys } from '../secretSnapshots/queries';
|
||||||
|
import {
|
||||||
|
BatchSecretDTO,
|
||||||
|
DecryptedSecret,
|
||||||
|
EncryptedSecret,
|
||||||
|
EncryptedSecretVersion,
|
||||||
|
GetProjectSecretsDTO,
|
||||||
|
GetSecretVersionsDTO
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const secretKeys = {
|
||||||
|
// this is also used in secretSnapshot part
|
||||||
|
getProjectSecret: (workspaceId: string, env: string) => [{ workspaceId, env }, 'secrets'],
|
||||||
|
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
|
params: {
|
||||||
|
environment: env,
|
||||||
|
workspaceId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data.secrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetProjectSecrets = ({
|
||||||
|
workspaceId,
|
||||||
|
env,
|
||||||
|
decryptFileKey,
|
||||||
|
isPaused
|
||||||
|
}: GetProjectSecretsDTO) =>
|
||||||
|
useQuery({
|
||||||
|
// wait for all values to be available
|
||||||
|
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||||
|
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
||||||
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
||||||
|
select: (data) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
|
const latestKey = decryptFileKey;
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const sharedSecrets: DecryptedSecret[] = [];
|
||||||
|
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||||
|
// this used for add-only mode in dashboard
|
||||||
|
// type won't be there thus only one key is shown
|
||||||
|
const duplicateSecretKey: Record<string, boolean> = {};
|
||||||
|
data.forEach((encSecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretKeyCiphertext,
|
||||||
|
iv: encSecret.secretKeyIV,
|
||||||
|
tag: encSecret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretValueCiphertext,
|
||||||
|
iv: encSecret.secretValueIV,
|
||||||
|
tag: encSecret.secretValueTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretComment = decryptSymmetric({
|
||||||
|
ciphertext: encSecret.secretCommentCiphertext,
|
||||||
|
iv: encSecret.secretCommentIV,
|
||||||
|
tag: encSecret.secretCommentTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSecret = {
|
||||||
|
_id: encSecret._id,
|
||||||
|
env: encSecret.environment,
|
||||||
|
key: secretKey,
|
||||||
|
value: secretValue,
|
||||||
|
tags: encSecret.tags,
|
||||||
|
comment: secretComment,
|
||||||
|
createdAt: encSecret.createdAt,
|
||||||
|
updatedAt: encSecret.updatedAt
|
||||||
|
};
|
||||||
|
|
||||||
|
if (encSecret.type === 'personal') {
|
||||||
|
personalSecrets[decryptedSecret.key] = { id: encSecret._id, value: secretValue };
|
||||||
|
} else {
|
||||||
|
if (!duplicateSecretKey?.[decryptedSecret.key]) {
|
||||||
|
sharedSecrets.push(decryptedSecret);
|
||||||
|
}
|
||||||
|
duplicateSecretKey[decryptedSecret.key] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sharedSecrets.forEach((val) => {
|
||||||
|
if (personalSecrets?.[val.key]) {
|
||||||
|
val.idOverride = personalSecrets[val.key].id;
|
||||||
|
val.valueOverride = personalSecrets[val.key].value;
|
||||||
|
val.overrideAction = 'modified';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secrets: sharedSecrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||||
|
const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
|
||||||
|
`/api/v1/secret/${secretId}/secret-versions`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data.secretVersions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
||||||
|
useQuery({
|
||||||
|
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
||||||
|
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
||||||
|
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
||||||
|
select: (data) => {
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
|
const latestKey = dto.decryptFileKey;
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: latestKey.encryptedKey,
|
||||||
|
nonce: latestKey.nonce,
|
||||||
|
publicKey: latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
return data
|
||||||
|
.map((el) => ({
|
||||||
|
createdAt: el.createdAt,
|
||||||
|
id: el._id,
|
||||||
|
value: decryptSymmetric({
|
||||||
|
ciphertext: el.secretValueCiphertext,
|
||||||
|
iv: el.secretValueIV,
|
||||||
|
tag: el.secretValueTag,
|
||||||
|
key
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useBatchSecretsOp = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, BatchSecretDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.post('/api/v2/secrets/batch', dto);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, dto) => {
|
||||||
|
queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment));
|
||||||
|
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
|
||||||
|
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import type { UserWsKeyPair } from '../keys/types';
|
||||||
|
import type { WsTag } from '../tags/types';
|
||||||
|
|
||||||
|
export type EncryptedSecret = {
|
||||||
|
_id: string;
|
||||||
|
version: number;
|
||||||
|
workspace: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
environment: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
__v: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
secretCommentCiphertext: string;
|
||||||
|
secretCommentIV: string;
|
||||||
|
secretCommentTag: string;
|
||||||
|
tags: WsTag[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DecryptedSecret = {
|
||||||
|
_id: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
comment: string;
|
||||||
|
tags: WsTag[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
env: string;
|
||||||
|
valueOverride?: string;
|
||||||
|
idOverride?: string;
|
||||||
|
overrideAction?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EncryptedSecretVersion = {
|
||||||
|
_id: string;
|
||||||
|
secret: string;
|
||||||
|
version: number;
|
||||||
|
workspace: string;
|
||||||
|
type: string;
|
||||||
|
environment: string;
|
||||||
|
isDeleted: boolean;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
tags: WsTag[];
|
||||||
|
__v: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// dto
|
||||||
|
type SecretTagArg = { _id: string; name: string; slug: string };
|
||||||
|
|
||||||
|
export type UpdateSecretArg = {
|
||||||
|
_id: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretCommentCiphertext: string;
|
||||||
|
secretCommentIV: string;
|
||||||
|
secretCommentTag: string;
|
||||||
|
tags: SecretTagArg[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateSecretArg = Omit<UpdateSecretArg, '_id'>;
|
||||||
|
|
||||||
|
export type DeleteSecretArg = { _id: string };
|
||||||
|
|
||||||
|
export type BatchSecretDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
requests: Array<
|
||||||
|
| { method: 'POST'; secret: CreateSecretArg }
|
||||||
|
| { method: 'PATCH'; secret: UpdateSecretArg }
|
||||||
|
| { method: 'DELETE'; secret: DeleteSecretArg }
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetProjectSecretsDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
env: string;
|
||||||
|
decryptFileKey: UserWsKeyPair;
|
||||||
|
isPaused?: boolean;
|
||||||
|
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSecretVersionsDTO = {
|
||||||
|
secretId: string;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
decryptFileKey: UserWsKeyPair;
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ export type { UserWsKeyPair } from './keys/types';
|
|||||||
export type { Organization } from './organization/types';
|
export type { Organization } from './organization/types';
|
||||||
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
|
||||||
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
|
||||||
|
export type { WsTag } from './tags/types';
|
||||||
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
|
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
|
||||||
export type {
|
export type {
|
||||||
CreateEnvironmentDTO,
|
CreateEnvironmentDTO,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export {
|
|||||||
useDeleteOrgMembership,
|
useDeleteOrgMembership,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetUser,
|
useGetUser,
|
||||||
|
useGetUserAction,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
|
useRegisterUserAction,
|
||||||
useUpdateOrgUserRole
|
useUpdateOrgUserRole
|
||||||
} from './queries';
|
} from './queries';
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
|
|
||||||
const userKeys = {
|
const userKeys = {
|
||||||
getUser: ['user'] as const,
|
getUser: ['user'] as const,
|
||||||
|
userAction: ['user-action'] as const,
|
||||||
getOrgUsers: (orgId: string) => [{ orgId }, 'user']
|
getOrgUsers: (orgId: string) => [{ orgId }, 'user']
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +32,21 @@ const fetchUserDetails = async () => {
|
|||||||
|
|
||||||
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
|
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
|
||||||
|
|
||||||
|
const fetchUserAction = async (action: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ userAction: string }>('/api/v1/user-action', {
|
||||||
|
params: {
|
||||||
|
action
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data.userAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetUserAction = (action: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: userKeys.userAction,
|
||||||
|
queryFn: () => fetchUserAction(action)
|
||||||
|
});
|
||||||
|
|
||||||
export const fetchOrgUsers = async (orgId: string) => {
|
export const fetchOrgUsers = async (orgId: string) => {
|
||||||
const { data } = await apiRequest.get<{ users: OrgUser[] }>(
|
const { data } = await apiRequest.get<{ users: OrgUser[] }>(
|
||||||
`/api/v1/organization/${orgId}/users`
|
`/api/v1/organization/${orgId}/users`
|
||||||
@@ -128,6 +144,16 @@ export const useUpdateOrgUserRole = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useRegisterUserAction = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<{}, {}, string>({
|
||||||
|
mutationFn: (action) => apiRequest.post('/api/v1/user-action', { action }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(userKeys.userAction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useLogoutUser = () =>
|
export const useLogoutUser = () =>
|
||||||
useMutation({
|
useMutation({
|
||||||
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
|
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ export {
|
|||||||
useDeleteWsEnvironment,
|
useDeleteWsEnvironment,
|
||||||
useGetUserWorkspaceMemberships,
|
useGetUserWorkspaceMemberships,
|
||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
|
useGetUserWsEnvironments,
|
||||||
useGetWorkspaceById,
|
useGetWorkspaceById,
|
||||||
useRenameWorkspace,
|
useRenameWorkspace,
|
||||||
useToggleAutoCapitalization,
|
useToggleAutoCapitalization,
|
||||||
useUpdateWsEnvironment} from './queries';
|
useUpdateWsEnvironment
|
||||||
|
} from './queries';
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ import {
|
|||||||
CreateWorkspaceDTO,
|
CreateWorkspaceDTO,
|
||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
|
GetWsEnvironmentDTO,
|
||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
Workspace
|
Workspace,
|
||||||
|
WorkspaceEnv
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const workspaceKeys = {
|
const workspaceKeys = {
|
||||||
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
|
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
|
||||||
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
|
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
|
||||||
getAllUserWorkspace: ['workspaces'] as const
|
getAllUserWorkspace: ['workspaces'] as const,
|
||||||
|
getUserWsEnvironments: (workspaceId: string) => ['workspace-env', { workspaceId }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
@@ -49,6 +52,21 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => {
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchUserWsEnvironments = async (workspaceId: string) => {
|
||||||
|
const { data } = await apiRequest.get<{ accessibleEnvironments: WorkspaceEnv[] }>(
|
||||||
|
`/api/v2/workspace/${workspaceId}/environments`
|
||||||
|
);
|
||||||
|
return data.accessibleEnvironments;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetUserWsEnvironments = ({ workspaceId, onSuccess }: GetWsEnvironmentDTO) =>
|
||||||
|
useQuery({
|
||||||
|
enabled: Boolean(workspaceId),
|
||||||
|
onSuccess,
|
||||||
|
queryKey: workspaceKeys.getUserWsEnvironments(workspaceId),
|
||||||
|
queryFn: () => fetchUserWsEnvironments(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
// to get all userids in an org with the workspace they are part of
|
// to get all userids in an org with the workspace they are part of
|
||||||
export const useGetUserWorkspaceMemberships = (orgId: string) =>
|
export const useGetUserWorkspaceMemberships = (orgId: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ export type Workspace = {
|
|||||||
environments: WorkspaceEnv[];
|
environments: WorkspaceEnv[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceEnv = { name: string; slug: string };
|
export type WorkspaceEnv = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
isReadDenied: boolean;
|
||||||
|
isWriteDenied: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkspaceTag = { _id: string; name: string; slug: string };
|
export type WorkspaceTag = { _id: string; name: string; slug: string };
|
||||||
|
|
||||||
// mutation dto
|
// mutation dto
|
||||||
@@ -16,6 +22,11 @@ export type CreateWorkspaceDTO = {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetWsEnvironmentDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
onSuccess?: (data: WorkspaceEnv[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { useLeaveConfirm } from './useLeaveConfirm';
|
export { useLeaveConfirm } from './useLeaveConfirm';
|
||||||
|
export { usePersistentState } from './usePersistentState';
|
||||||
export { usePopUp } from './usePopUp';
|
export { usePopUp } from './usePopUp';
|
||||||
export { useToggle } from './useToggle';
|
export { useToggle } from './useToggle';
|
||||||
|
|||||||
@@ -4,52 +4,56 @@ import { useRouter } from 'next/router';
|
|||||||
import { leaveConfirmDefaultMessage } from '@app/const';
|
import { leaveConfirmDefaultMessage } from '@app/const';
|
||||||
|
|
||||||
type LeaveConfirmProps = {
|
type LeaveConfirmProps = {
|
||||||
initialValue: boolean,
|
initialValue: boolean;
|
||||||
message?: string
|
message?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface LeaveConfirmReturn {
|
interface LeaveConfirmReturn {
|
||||||
hasUnsavedChanges: boolean,
|
hasUnsavedChanges: boolean;
|
||||||
setHasUnsavedChanges: Dispatch<SetStateAction<boolean>>,
|
setHasUnsavedChanges: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLeaveConfirm({
|
export function useLeaveConfirm({
|
||||||
initialValue,
|
initialValue,
|
||||||
message = leaveConfirmDefaultMessage,
|
message = leaveConfirmDefaultMessage
|
||||||
}: LeaveConfirmProps): LeaveConfirmReturn {
|
}: LeaveConfirmProps): LeaveConfirmReturn {
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(initialValue);
|
||||||
|
|
||||||
const onRouteChangeStart = useCallback(() => {
|
const onRouteChangeStart = useCallback(() => {
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
if (window.confirm(message)) {
|
if (window.confirm(message)) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
throw new Error("Abort route change by user's confirmation.")
|
throw new Error("Abort route change by user's confirmation.");
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}, [hasUnsavedChanges])
|
}, [hasUnsavedChanges]);
|
||||||
|
|
||||||
const handleWindowClose = useCallback((e: any) => {
|
const handleWindowClose = useCallback(
|
||||||
if (!hasUnsavedChanges) {
|
(e: any) => {
|
||||||
return;
|
if (!hasUnsavedChanges) {
|
||||||
}
|
return;
|
||||||
e.preventDefault();
|
}
|
||||||
e.returnValue = message;
|
e.preventDefault();
|
||||||
}, []);
|
e.returnValue = message;
|
||||||
|
},
|
||||||
|
[hasUnsavedChanges]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.events.on("routeChangeStart", onRouteChangeStart);
|
router.events.on('routeChangeStart', onRouteChangeStart);
|
||||||
window.addEventListener('beforeunload', handleWindowClose);
|
window.addEventListener('beforeunload', handleWindowClose);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeStart", onRouteChangeStart);
|
router.events.off('routeChangeStart', onRouteChangeStart);
|
||||||
window.removeEventListener('beforeunload', handleWindowClose);
|
window.removeEventListener('beforeunload', handleWindowClose);
|
||||||
}
|
};
|
||||||
}, [onRouteChangeStart, handleWindowClose]);
|
}, [onRouteChangeStart, handleWindowClose]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
setHasUnsavedChanges,
|
setHasUnsavedChanges
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
type TPersisntentStateReturn<T extends unknown> = [T, (val: T) => void];
|
||||||
|
|
||||||
|
export const usePersistentState = <T extends unknown>(
|
||||||
|
initialValue: T,
|
||||||
|
persistenceKey: string
|
||||||
|
): TPersisntentStateReturn<T> => {
|
||||||
|
const [val, setVal] = useState<T>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const temp = localStorage.getItem(persistenceKey);
|
||||||
|
if (temp) {
|
||||||
|
const { key } = JSON.parse(temp);
|
||||||
|
setVal(key);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setState = (state: T) => {
|
||||||
|
localStorage.setItem(persistenceKey, JSON.stringify({ key: state }));
|
||||||
|
setVal(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [val, setState];
|
||||||
|
};
|
||||||
+18
-15
@@ -8,6 +8,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
|
|||||||
|
|
||||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||||
|
import { TooltipProvider } from '@app/components/v2';
|
||||||
import { publicPaths } from '@app/const';
|
import { publicPaths } from '@app/const';
|
||||||
import {
|
import {
|
||||||
AuthProvider,
|
AuthProvider,
|
||||||
@@ -73,21 +74,23 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<TooltipProvider>
|
||||||
<WorkspaceProvider>
|
<AuthProvider>
|
||||||
<OrgProvider>
|
<WorkspaceProvider>
|
||||||
<SubscriptionProvider>
|
<OrgProvider>
|
||||||
<UserProvider>
|
<SubscriptionProvider>
|
||||||
<NotificationProvider>
|
<UserProvider>
|
||||||
<AppLayout>
|
<NotificationProvider>
|
||||||
<Component {...pageProps} />
|
<AppLayout>
|
||||||
</AppLayout>
|
<Component {...pageProps} />
|
||||||
</NotificationProvider>
|
</AppLayout>
|
||||||
</UserProvider>
|
</NotificationProvider>
|
||||||
</SubscriptionProvider>
|
</UserProvider>
|
||||||
</OrgProvider>
|
</SubscriptionProvider>
|
||||||
</WorkspaceProvider>
|
</OrgProvider>
|
||||||
</AuthProvider>
|
</WorkspaceProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,42 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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/400.css';
|
||||||
@import '@fontsource/inter/500.css';
|
@import '@fontsource/inter/500.css';
|
||||||
@import '@fontsource/inter/700.css';
|
@import '@fontsource/inter/700.css';
|
||||||
|
|||||||
@@ -0,0 +1,674 @@
|
|||||||
|
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, 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(true);
|
||||||
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
|
const [snapshotId, setSnaphotId] = useState<string | null>(null);
|
||||||
|
const [selectedEnv, setSelectedEnv] = useState<WorkspaceEnv | null>(null);
|
||||||
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
|
||||||
|
const [deletedSecretIds, setDeletedSecretIds] = useState<string[]>([]);
|
||||||
|
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<FormData>({
|
||||||
|
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||||
|
defaultValues: secrets as any,
|
||||||
|
values: secrets as any,
|
||||||
|
mode: 'onBlur',
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
|
formState: { 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 && Object.keys(dirtyFields).length === 0) ||
|
||||||
|
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<Record<string, boolean>>(
|
||||||
|
(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 (
|
||||||
|
<div className="container mx-auto flex h-1/2 w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<FormProvider {...method}>
|
||||||
|
<form autoComplete="off">
|
||||||
|
{/* breadcrumb row */}
|
||||||
|
<div className="relative right-5">
|
||||||
|
<NavHeader pageName={t('dashboard:title')} isProjectRelated />
|
||||||
|
</div>
|
||||||
|
{/* Secrets, commit and save button section */}
|
||||||
|
<div className="mt-6 flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<h1 className="text-3xl font-semibold">
|
||||||
|
{isRollbackMode ? 'Secret Snapshot' : 'Secrets'}
|
||||||
|
</h1>
|
||||||
|
{isRollbackMode && Boolean(snapshotSecret) && (
|
||||||
|
<Tag colorSchema="green">
|
||||||
|
{new Date(snapshotSecret?.createdAt || '').toLocaleString()}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isRollbackMode && (
|
||||||
|
<Button
|
||||||
|
variant="star"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||||
|
onClick={() => {
|
||||||
|
setSnaphotId(null);
|
||||||
|
reset({ ...secrets, isSnapshotMode: false });
|
||||||
|
}}
|
||||||
|
className='h-10'
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="star"
|
||||||
|
onClick={() => handlePopUpOpen('secretSnapshots')}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||||
|
isLoading={isLoadingSnapshotCount}
|
||||||
|
isDisabled={!canDoRollback}
|
||||||
|
className='h-10'
|
||||||
|
>
|
||||||
|
{snapshotCount} Commits
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isDisabled={isSubmitDisabled}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
||||||
|
onClick={handleSubmit(onSaveSecret)}
|
||||||
|
className='h-10'
|
||||||
|
>
|
||||||
|
{isRollbackMode ? 'Rollback' : 'Save Changes'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Environment, search and other action row */}
|
||||||
|
<div className="mt-4 flex items-center space-x-2">
|
||||||
|
<div>
|
||||||
|
<Tooltip content="Select environment">
|
||||||
|
<Select
|
||||||
|
value={selectedEnv?.slug}
|
||||||
|
onValueChange={onEnvChange}
|
||||||
|
position="popper"
|
||||||
|
className="min-w-[180px] bg-mineshaft-600 h-10 font-medium"
|
||||||
|
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
|
||||||
|
>
|
||||||
|
{userAvailableEnvs?.map(({ name, slug }) => (
|
||||||
|
<SelectItem value={slug} key={slug}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Input
|
||||||
|
className="bg-mineshaft-600 h-[2.3rem] placeholder-mineshaft-50"
|
||||||
|
placeholder="Search keys..."
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<IconButton ariaLabel="download" variant="star">
|
||||||
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto bg-mineshaft-800 border border-mineshaft-600 p-1" hideCloseBtn>
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => downloadSecret(getValues('secrets'), selectedEnv?.slug)}
|
||||||
|
variant="star"
|
||||||
|
className="bg-bunker-700 h-8"
|
||||||
|
>
|
||||||
|
Download as .env
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip content={isSecretValueHidden ? 'Reveal Secrets' : 'Hide secrets'}>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="reveal"
|
||||||
|
variant="star"
|
||||||
|
onClick={() => setIsSecretValueHidden.toggle()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isSecretValueHidden ? faEye : faEyeSlash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{!isReadOnly && !isRollbackMode && <Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false })}
|
||||||
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
|
variant="star"
|
||||||
|
className="h-10"
|
||||||
|
>
|
||||||
|
Add Secret
|
||||||
|
</Button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${isSecretEmpty ? "flex flex-col items-center justify-center" : ""} mt-4 h-[calc(100vh-270px)] overflow-y-scroll overflow-x-hidden no-scrollbar no-scrollbar::-webkit-scrollbar`}>
|
||||||
|
{!isSecretEmpty && (
|
||||||
|
<TableContainer>
|
||||||
|
<table className="secret-table relative">
|
||||||
|
<SecretTableHeader
|
||||||
|
sortDir={sortDir}
|
||||||
|
onSort={onSortSecrets}
|
||||||
|
/>
|
||||||
|
<tbody className="overflow-y-auto max-h-screen">
|
||||||
|
{fields.map(({ id, _id }, index) => (
|
||||||
|
<SecretInputRow
|
||||||
|
key={id}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
isRollbackMode={isRollbackMode}
|
||||||
|
isAddOnly={isAddOnly}
|
||||||
|
index={index}
|
||||||
|
searchTerm={searchFilter}
|
||||||
|
onSecretDelete={onSecretDelete}
|
||||||
|
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||||
|
isSecretValueHidden={isSecretValueHidden}
|
||||||
|
wsTags={wsTags}
|
||||||
|
onCreateTagOpen={() => handlePopUpOpen('addTag')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!isReadOnly && !isRollbackMode && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-[calc(100vw-400px)] h-8 ml-12 font-normal text-bunker-300 flex justify-start items-center"
|
||||||
|
onClick={onAppendSecret}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
<span className="w-20 ml-2">Add Secret</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
<PitDrawer
|
||||||
|
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle('secretSnapshots', isOpen)}
|
||||||
|
fetchNextPage={fetchNextPage}
|
||||||
|
hasNextPage={hasNextPage}
|
||||||
|
snapshotId={snapshotId}
|
||||||
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
secretSnaphots={secretSnaphots}
|
||||||
|
onSelectSnapshot={setSnaphotId}
|
||||||
|
/>
|
||||||
|
<SecretDetailDrawer
|
||||||
|
onSave={handleSubmit(onSaveSecret)}
|
||||||
|
isReadOnly={isReadOnly || isRollbackMode}
|
||||||
|
onSecretDelete={onSecretDelete}
|
||||||
|
isDrawerOpen={popUp?.secretDetails?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle('secretDetails', isOpen)}
|
||||||
|
secretVersion={secretVersion}
|
||||||
|
index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index}
|
||||||
|
onEnvCompare={(key) => handlePopUpOpen('compareSecrets', key)}
|
||||||
|
/>
|
||||||
|
<SecretDropzone
|
||||||
|
isSmaller={!isSecretEmpty}
|
||||||
|
onParsedEnv={handleUploadedEnv}
|
||||||
|
onAddNewSecret={onAppendSecret}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* secrets table and drawers, modals */}
|
||||||
|
</form>
|
||||||
|
{/* Create a new tag modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addTag?.isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
handlePopUpToggle('addTag', open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Create tag"
|
||||||
|
subTitle="Specify your tag name, and the slug will be created automatically."
|
||||||
|
>
|
||||||
|
<CreateTagModal onCreateTag={onCreateWsTag} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
{/* Uploaded env override or not confirmation modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.uploadedSecOpts?.isOpen}
|
||||||
|
onOpenChange={(open) => handlePopUpToggle('uploadedSecOpts', open)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Duplicate Secrets"
|
||||||
|
footerContent={[
|
||||||
|
<Button
|
||||||
|
key="keep-old-btn"
|
||||||
|
className="mr-4"
|
||||||
|
onClick={() => handlePopUpClose('uploadedSecOpts')}
|
||||||
|
>
|
||||||
|
Keep old
|
||||||
|
</Button>,
|
||||||
|
<Button colorSchema="danger" key="overwrite-btn" onClick={onOverwriteSecrets}>
|
||||||
|
Overwrite
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-2 text-gray-300">
|
||||||
|
<div>Your file contains following duplicate secrets</div>
|
||||||
|
<div className="text-sm text-gray-400">
|
||||||
|
{Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {})
|
||||||
|
?.map((key) => key)
|
||||||
|
.join(', ')}
|
||||||
|
</div>
|
||||||
|
<div>Are you sure you want to overwrite these secrets?</div>
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.compareSecrets?.isOpen}
|
||||||
|
onOpenChange={(open) => handlePopUpToggle('compareSecrets', open)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={popUp?.compareSecrets?.data as string}
|
||||||
|
subTitle="Below is the comparison of secret values across available environments"
|
||||||
|
overlayClassName="z-[90]"
|
||||||
|
>
|
||||||
|
<CompareSecret
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
envs={userAvailableEnvs || []}
|
||||||
|
secretKey={popUp?.compareSecrets?.data as string}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</FormProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<number, boolean> = {};
|
||||||
|
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<typeof schema>;
|
||||||
|
export type TSecretDetailsOpen = { index: number; id: string };
|
||||||
|
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<FormControl label={envName}>
|
||||||
|
<Input
|
||||||
|
className={`w-full text-ellipsis font-mono focus:ring-transparent ${getValue(secret) === "Not found" && "text-mineshaft-500"}`}
|
||||||
|
value={getValue(secret)}
|
||||||
|
isReadOnly
|
||||||
|
rightIcon={isSecretsLoading ? <Spinner /> : undefined}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 <div />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{envs.map(({ name, slug }) => (
|
||||||
|
<SecretValue
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
key={`secret-comparison-${slug}`}
|
||||||
|
envName={name}
|
||||||
|
env={slug}
|
||||||
|
secretKey={secretKey}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CompareSecret } from './CompareSecret';
|
||||||
@@ -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<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTagSchema = yup.object({
|
||||||
|
name: yup.string().required().trim().label('Tag Name')
|
||||||
|
});
|
||||||
|
type FormData = yup.InferType<typeof createTagSchema>;
|
||||||
|
|
||||||
|
export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: yupResolver(createTagSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name }: FormData) => {
|
||||||
|
await onCreateTag(name);
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} placeholder="Type your tag name" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {CreateTagModal} from './CreateTagModal'
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
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<TWorkspaceSecretSnapshot[]>;
|
||||||
|
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 (
|
||||||
|
<Drawer isOpen={isDrawerOpen} onOpenChange={onOpenChange}>
|
||||||
|
<DrawerContent
|
||||||
|
title="Point-in-time Recovery"
|
||||||
|
subTitle="Note: This will recover secrets for all environments in this project"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
{secretSnaphots?.pages?.map((group, i) => (
|
||||||
|
<Fragment key={`snapshot-item-${i + 1}`}>
|
||||||
|
{group.map(({ _id, createdAt }, index) => (
|
||||||
|
<Button
|
||||||
|
key={_id}
|
||||||
|
className="py-3 px-4 text-sm"
|
||||||
|
isFullWidth
|
||||||
|
colorSchema={
|
||||||
|
(i === 0 && index === 0 && snapshotId === null) || snapshotId === _id
|
||||||
|
? 'primary'
|
||||||
|
: 'secondary'
|
||||||
|
}
|
||||||
|
variant={
|
||||||
|
(i === 0 && index === 0 && snapshotId === null) || snapshotId === _id
|
||||||
|
? 'selected'
|
||||||
|
: 'star'
|
||||||
|
}
|
||||||
|
onClick={() => onSelectSnapshot(_id)}
|
||||||
|
>
|
||||||
|
<div className="flex w-full justify-between">
|
||||||
|
<div>{timeSince(new Date(createdAt))}</div>
|
||||||
|
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === _id)}</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mt-8 py-3 px-4 text-sm"
|
||||||
|
isFullWidth
|
||||||
|
variant="star"
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
|
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||||
|
onClick={fetchNextPage}
|
||||||
|
>
|
||||||
|
{hasNextPage ? 'Load More' : 'End of history'}
|
||||||
|
</Button>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PitDrawer } from './PitDrawer';
|
||||||
@@ -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 (
|
||||||
|
<div className="flex">
|
||||||
|
<Button
|
||||||
|
className="rounded-r-none"
|
||||||
|
colorSchema="secondary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faShuffle} />}
|
||||||
|
onClick={onGenerateRandomHex}
|
||||||
|
>
|
||||||
|
{t('dashboard:sidebar.generate-random-hex')}
|
||||||
|
</Button>
|
||||||
|
<div className="rounded-r-md bg-mineshaft p-2">
|
||||||
|
<div className="flex rounded-md border-mineshaft-400 bg-bunker-800 p-1 px-2">
|
||||||
|
<button type="button" onClick={() => setValue((val) => val - 1)}>
|
||||||
|
<FontAwesomeIcon icon={faMinus} size="xs" />
|
||||||
|
</button>
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
className="w-8 p-0 text-center"
|
||||||
|
value={value}
|
||||||
|
variant="plain"
|
||||||
|
onChange={(e) => setValue(parseInt(e.target.value, 10))}
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={() => setValue((val) => val + 1)}>
|
||||||
|
<FontAwesomeIcon icon={faPlus} size="xs" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+211
@@ -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<FormData>();
|
||||||
|
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 <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer onOpenChange={onOpenChange} isOpen={isDrawerOpen}>
|
||||||
|
<DrawerContent
|
||||||
|
className="border-l border-mineshaft-500 bg-bunker dark"
|
||||||
|
title="Secret"
|
||||||
|
footerContent={
|
||||||
|
<div className="flex flex-col space-y-2 pt-4 shadow-md">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="star"
|
||||||
|
onClick={() => onEnvCompare(secret?.key)}
|
||||||
|
isFullWidth
|
||||||
|
isDisabled={isReadOnly}
|
||||||
|
>
|
||||||
|
Compare secret across environments
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full space-x-2">
|
||||||
|
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="danger"
|
||||||
|
isDisabled={isReadOnly}
|
||||||
|
onClick={() => onSecretDelete(index, secret._id, secret.idOverride)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="dark:[color-scheme:dark]">
|
||||||
|
<FormControl label="Key">
|
||||||
|
<Input isDisabled {...register(`secrets.${index}.key`)} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl label="Value">
|
||||||
|
<Popover>
|
||||||
|
<Input
|
||||||
|
isReadOnly={isOverridden || isReadOnly}
|
||||||
|
{...register(`secrets.${index}.value`)}
|
||||||
|
placeholder="EMPTY"
|
||||||
|
onBlur={setCanRevealSecVal.off}
|
||||||
|
onFocus={setCanRevealSecVal.on}
|
||||||
|
type={canRevealSecVal ? 'text' : 'password'}
|
||||||
|
rightIcon={
|
||||||
|
<PopoverTrigger disabled={isOverridden || isReadOnly}>
|
||||||
|
<FontAwesomeIcon icon={faShuffle} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PopoverContent
|
||||||
|
hideCloseBtn
|
||||||
|
className="w-auto border-mineshaft-500 bg-bunker p-0"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<GenRandomNumber
|
||||||
|
onGenerate={(val) =>
|
||||||
|
setValue(`secrets.${index}.value`, val, { shouldDirty: true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormControl>
|
||||||
|
<div className="mb-2 border-t border-mineshaft-600 pt-4">
|
||||||
|
<Switch
|
||||||
|
id="personal-override"
|
||||||
|
onCheckedChange={onSecretOverride}
|
||||||
|
isChecked={isOverridden}
|
||||||
|
isDisabled={isReadOnly}
|
||||||
|
>
|
||||||
|
Override with a personal value
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Popover>
|
||||||
|
<Input
|
||||||
|
isReadOnly={!isOverridden || isReadOnly}
|
||||||
|
{...register(`secrets.${index}.valueOverride`)}
|
||||||
|
placeholder="EMPTY"
|
||||||
|
type={canRevealSecOverride ? 'text' : 'password'}
|
||||||
|
onBlur={setCanRevealSecOverride.off}
|
||||||
|
onFocus={setCanRevealSecOverride.on}
|
||||||
|
rightIcon={
|
||||||
|
<PopoverTrigger disabled={!isOverridden || isReadOnly}>
|
||||||
|
<FontAwesomeIcon icon={faShuffle} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PopoverContent
|
||||||
|
hideCloseBtn
|
||||||
|
className="w-auto border-mineshaft-500 bg-bunker p-0"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<GenRandomNumber
|
||||||
|
onGenerate={(val) =>
|
||||||
|
setValue(`secrets.${index}.valueOverride`, val, { shouldDirty: true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</FormControl>
|
||||||
|
<div className="mb-4 text-sm text-bunker-300 dark">
|
||||||
|
<div className="mb-2">Version History</div>
|
||||||
|
<div className="flex h-48 flex-col space-y-2 border border-mineshaft-600 overflow-y-auto overflow-x-hidden rounded-md bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||||
|
{secretVersion?.map(({ createdAt, value, id }, i) => (
|
||||||
|
<div key={id} className="flex flex-col space-y-1">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{new Date(createdAt).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||||
|
<div className="rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||||
|
<div className="font-mono">{value}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl label="Comments & Notes">
|
||||||
|
<TextArea className="border border-mineshaft-600 text-sm" isDisabled={isReadOnly} {...register(`secrets.${index}.comment`)} rows={5} />
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export {SecretDetailDrawer} from './SecretDetailDrawer'
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { ChangeEvent, DragEvent } from 'react';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
|
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionalityj
|
||||||
|
import { parseDotEnv } from '@app/components/utilities/parseDotEnv';
|
||||||
|
import { Button } from '@app/components/v2';
|
||||||
|
import { useToggle } from '@app/hooks/useToggle';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isSmaller: boolean;
|
||||||
|
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
|
||||||
|
onAddNewSecret?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isDragActive, setDragActive] = useToggle();
|
||||||
|
const [isLoading, setIsLoading] = useToggle();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const handleDrag = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||||
|
setDragActive.on();
|
||||||
|
} else if (e.type === 'dragleave') {
|
||||||
|
setDragActive.off();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseFile = (file?: File) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
if (!file) {
|
||||||
|
createNotification({
|
||||||
|
text: `You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.`,
|
||||||
|
type: 'error',
|
||||||
|
timeoutMs: 10000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// const fileType = file.name.split('.')[1];
|
||||||
|
setIsLoading.on();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
if (!event?.target?.result) return;
|
||||||
|
// parse function's argument looks like to be ArrayBuffer
|
||||||
|
const env = parseDotEnv(event.target.result as ArrayBuffer);
|
||||||
|
setIsLoading.off();
|
||||||
|
onParsedEnv(env);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If something is wrong show an error
|
||||||
|
try {
|
||||||
|
reader.readAsText(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!e.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
setDragActive.off();
|
||||||
|
parseFile(e.dataTransfer.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
parseFile(e.target?.files?.[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onDragEnter={handleDrag}
|
||||||
|
onDragLeave={handleDrag}
|
||||||
|
onDragOver={handleDrag}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
className={twMerge(
|
||||||
|
'relative mb-4 mt-4 max-w-[calc(100vw-292px)] flex w-full cursor-pointer text-mineshaft-200 items-center py-8 justify-center space-x-2 rounded-md bg-mineshaft-900 px-2 mx-0.5 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100',
|
||||||
|
isDragActive && 'opacity-100',
|
||||||
|
!isSmaller && 'flex-col space-y-4 max-w-3xl py-20',
|
||||||
|
isLoading && 'bg-bunker-800'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="mb-16 flex items-center justify-center pt-16">
|
||||||
|
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faUpload} size={isSmaller ? '2x' : '5x'} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="">{t(isSmaller ? 'common:drop-zone-keys' : 'common:drop-zone')}</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="fileSelect"
|
||||||
|
type="file"
|
||||||
|
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||||
|
accept=".txt,.env,.yml,.yaml"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
{!isSmaller && (
|
||||||
|
<>
|
||||||
|
<div className="flex w-full flex-row items-center justify-center py-4">
|
||||||
|
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||||
|
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||||
|
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button variant="star" onClick={onAddNewSecret}>
|
||||||
|
Add a new secret
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretDropzone } from './SecretDropzone';
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
|
import { SyntheticEvent, useRef } from 'react';
|
||||||
|
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||||
|
import {
|
||||||
|
faCircle,
|
||||||
|
faCodeBranch,
|
||||||
|
faComment,
|
||||||
|
faEllipsis,
|
||||||
|
faInfoCircle,
|
||||||
|
faPlus,
|
||||||
|
faTags,
|
||||||
|
faXmark
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { cx } from 'cva';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
import guidGenerator from '@app/components/utilities/randomId';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Tag,
|
||||||
|
TextArea,
|
||||||
|
Tooltip
|
||||||
|
} from '@app/components/v2';
|
||||||
|
import { useToggle } from '@app/hooks';
|
||||||
|
import { WsTag } from '@app/hooks/api/types';
|
||||||
|
|
||||||
|
import { FormData, SecretActionType } from '../../DashboardPage.utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
index: number;
|
||||||
|
// permission and external state's that decided to hide or show
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
isAddOnly?: boolean;
|
||||||
|
isRollbackMode?: boolean;
|
||||||
|
isSecretValueHidden: boolean;
|
||||||
|
searchTerm: string;
|
||||||
|
// to record the ids of deleted ones
|
||||||
|
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
|
||||||
|
// sidebar control props
|
||||||
|
onRowExpand: () => void;
|
||||||
|
// tag props
|
||||||
|
wsTags?: WsTag[];
|
||||||
|
onCreateTagOpen: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagColors = [
|
||||||
|
{ bg: 'bg-[#f1c40f]/40', text: 'text-[#fcf0c3]/70' },
|
||||||
|
{ bg: 'bg-[#cb1c8d]/40', text: 'text-[#f2c6e3]/70' },
|
||||||
|
{ bg: 'bg-[#badc58]/40', text: 'text-[#eef6d5]/70' },
|
||||||
|
{ bg: 'bg-[#ff5400]/40', text: 'text-[#ffddcc]/70' },
|
||||||
|
{ bg: 'bg-[#3AB0FF]/40', text: 'text-[#f0fffd]/70' },
|
||||||
|
{ bg: 'bg-[#6F1AB6]/40', text: 'text-[#FFE5F1]/70' },
|
||||||
|
{ bg: 'bg-[#C40B13]/40', text: 'text-[#FFDEDE]/70' },
|
||||||
|
{ bg: 'bg-[#332FD0]/40', text: 'text-[#DFF6FF]/70' }
|
||||||
|
];
|
||||||
|
const REGEX = /([$]{.*?})/g;
|
||||||
|
|
||||||
|
export const SecretInputRow = ({
|
||||||
|
index,
|
||||||
|
isSecretValueHidden,
|
||||||
|
onRowExpand,
|
||||||
|
isReadOnly,
|
||||||
|
isRollbackMode,
|
||||||
|
isAddOnly,
|
||||||
|
wsTags,
|
||||||
|
onCreateTagOpen,
|
||||||
|
onSecretDelete,
|
||||||
|
searchTerm
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
|
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||||
|
if (ref.current === null) return;
|
||||||
|
|
||||||
|
ref.current.scrollTop = e.currentTarget.scrollTop;
|
||||||
|
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||||
|
};
|
||||||
|
const { register, setValue, control } = useFormContext<FormData>();
|
||||||
|
const [canRevealSecret] = useToggle();
|
||||||
|
// comment management in a row
|
||||||
|
const {
|
||||||
|
fields: secretTags,
|
||||||
|
remove,
|
||||||
|
append
|
||||||
|
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||||
|
|
||||||
|
// to get details on a secret
|
||||||
|
const secret = useWatch({ name: `secrets.${index}`, control });
|
||||||
|
const hasComment = Boolean(secret.comment);
|
||||||
|
const tags = secret.tags || [];
|
||||||
|
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
// when secret is override by personal values
|
||||||
|
const isOverridden =
|
||||||
|
secret.overrideAction === SecretActionType.Created ||
|
||||||
|
secret.overrideAction === SecretActionType.Modified;
|
||||||
|
|
||||||
|
const onSecretOverride = () => {
|
||||||
|
if (isOverridden) {
|
||||||
|
// when user created a new override but then removes
|
||||||
|
if (secret?.overrideAction === SecretActionType.Created)
|
||||||
|
setValue(`secrets.${index}.valueOverride`, '');
|
||||||
|
setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true });
|
||||||
|
} else {
|
||||||
|
setValue(`secrets.${index}.valueOverride`, '');
|
||||||
|
setValue(
|
||||||
|
`secrets.${index}.overrideAction`,
|
||||||
|
secret?.idOverride ? SecretActionType.Modified : SecretActionType.Created,
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectTag = (selectedTag: WsTag) => {
|
||||||
|
const shouldAppend = !selectedTagIds[selectedTag.slug];
|
||||||
|
if (shouldAppend) {
|
||||||
|
append(selectedTag);
|
||||||
|
} else {
|
||||||
|
const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug);
|
||||||
|
remove(pos);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCreatedSecret = !secret?._id;
|
||||||
|
const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
|
||||||
|
|
||||||
|
// Why this instead of filter in parent
|
||||||
|
// Because rhf field.map has default values so basically
|
||||||
|
// keys are not updated there and index needs to kept so that we can monitor
|
||||||
|
// values individually here
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
secret.key?.toUpperCase().includes(searchTerm?.toUpperCase()) ||
|
||||||
|
tags
|
||||||
|
?.map((tag) => tag.name)
|
||||||
|
.join(' ')
|
||||||
|
?.toUpperCase()
|
||||||
|
.includes(searchTerm?.toUpperCase()) ||
|
||||||
|
secret.comment?.toUpperCase().includes(searchTerm?.toUpperCase())
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="group min-w-full flex flex-row items-center">
|
||||||
|
<td className="w-10 h-10 px-4 flex items-center justify-center"><div className='text-center w-10 text-xs text-bunker-400'>{index + 1}</div></td>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name={`secrets.${index}.key`}
|
||||||
|
render={({ fieldState: { error }, field }) => (
|
||||||
|
<HoverCard openDelay={0} open={error?.message ? undefined : false}>
|
||||||
|
<HoverCardTrigger asChild>
|
||||||
|
<td className={cx(error?.message ? 'rounded ring ring-red/50' : null)}>
|
||||||
|
<div className="min-w-[220px] lg:min-w-[240px] xl:min-w-[280px] relative flex items-center justify-end w-full">
|
||||||
|
<Input
|
||||||
|
autoComplete="off"
|
||||||
|
variant="plain"
|
||||||
|
isDisabled={isReadOnly || shouldBeBlockedInAddOnly || isRollbackMode}
|
||||||
|
className="w-full focus:text-bunker-100 focus:ring-transparent"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
<div className="w-max flex flex-row items-center justify-end">
|
||||||
|
<Tooltip content="Comment">
|
||||||
|
<div className={`${hasComment ? "w-5" : "w-0"} overflow-hidden group-hover:w-5 mt-0.5`}>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
className={twMerge(
|
||||||
|
'w-0 overflow-hidden p-0 group-hover:w-5',
|
||||||
|
hasComment && 'w-5 text-primary'
|
||||||
|
)}
|
||||||
|
variant="plain"
|
||||||
|
size="md"
|
||||||
|
ariaLabel="add-tag"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faComment} />
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl p-2">
|
||||||
|
<FormControl label="Comment" className="mb-0">
|
||||||
|
<TextArea
|
||||||
|
isDisabled={isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly}
|
||||||
|
className="border border-mineshaft-600 text-sm"
|
||||||
|
{...register(`secrets.${index}.comment`)}
|
||||||
|
rows={8}
|
||||||
|
cols={30}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
{!isAddOnly && (
|
||||||
|
<div>
|
||||||
|
<Tooltip content="Override with a personal value">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className={twMerge(
|
||||||
|
'w-0 overflow-hidden p-0 group-hover:w-6 group-hover:ml-1 mt-0.5',
|
||||||
|
isOverridden && 'w-6 text-primary ml-1'
|
||||||
|
)}
|
||||||
|
onClick={onSecretOverride}
|
||||||
|
size="md"
|
||||||
|
isDisabled={isRollbackMode || isReadOnly}
|
||||||
|
ariaLabel="info"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<FontAwesomeIcon icon={faCodeBranch} className="text-base" />
|
||||||
|
</div>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent className="w-auto py-2 pt-2">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">{error?.message}</div>
|
||||||
|
</div>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<td className="flex flex-row w-full justify-center h-8 items-center">
|
||||||
|
<div className="group relative whitespace-pre flex flex-col justify-center w-full px-1.5">
|
||||||
|
{isOverridden
|
||||||
|
? <input
|
||||||
|
{...register(`secrets.${index}.valueOverride`)}
|
||||||
|
onScroll={syncScroll}
|
||||||
|
readOnly={isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)}
|
||||||
|
className={`${
|
||||||
|
(!canRevealSecret && isSecretValueHidden)
|
||||||
|
? 'text-transparent focus:text-transparent active:text-transparent'
|
||||||
|
: ''
|
||||||
|
} 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"
|
||||||
|
/>
|
||||||
|
: <input
|
||||||
|
{...register(`secrets.${index}.value`)}
|
||||||
|
onScroll={syncScroll}
|
||||||
|
readOnly={isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)}
|
||||||
|
className={`${
|
||||||
|
(!canRevealSecret && isSecretValueHidden)
|
||||||
|
? 'text-transparent focus:text-transparent active:text-transparent'
|
||||||
|
: ''
|
||||||
|
} 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"
|
||||||
|
/>}
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`${
|
||||||
|
(!canRevealSecret && isSecretValueHidden) && !isOverridden
|
||||||
|
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400 duration-200'
|
||||||
|
: ''
|
||||||
|
} ${isOverridden ? 'text-primary-300' : 'text-gray-400'}
|
||||||
|
absolute flex flex-row whitespace-pre font-mono z-0 ${(!canRevealSecret && isSecretValueHidden) ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
|
>
|
||||||
|
{(isOverridden ? secret.valueOverride : secret.value)?.split('').length === 0 && <span className='text-bunker-400/80 font-sans'>EMPTY</span>}
|
||||||
|
{(isOverridden ? secret.valueOverride : secret.value)?.split(REGEX).map((word) => {
|
||||||
|
if (word.match(REGEX) !== null) {
|
||||||
|
return (
|
||||||
|
<span className="ph-no-capture text-yellow" key={index}>
|
||||||
|
{word.slice(0, 2)}
|
||||||
|
<span className="ph-no-capture text-yellow-200/80">
|
||||||
|
{word.slice(2, word.length - 1)}
|
||||||
|
</span>
|
||||||
|
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||||
|
<span className="ph-no-capture text-yellow">
|
||||||
|
{word.slice(word.length - 1, word.length)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="ph-no-capture text-yellow-400">
|
||||||
|
{word.slice(word.length - 1, word.length)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span key={`${word}_${index + 1}`} className="ph-no-capture">
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{(!canRevealSecret && isSecretValueHidden) && (
|
||||||
|
<div className='absolute flex flex-row justify-between items-center z-0 peer pr-2 peer-active:hidden peer-focus:hidden group-hover:bg-white/[0.00] duration-100 h-10 w-full text-bunker-400 text-clip'>
|
||||||
|
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||||
|
{(isOverridden ? secret.valueOverride : secret.value)?.split('').map(() => (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
key={guidGenerator()}
|
||||||
|
className="text-xxs mr-0.5"
|
||||||
|
icon={faCircle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{(isOverridden ? secret.valueOverride : secret.value)?.split('').length === 0 && <span className='text-bunker-400/80 text-sm'>EMPTY</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="flex items-center min-w-sm h-10">
|
||||||
|
<div className="flex items-center pl-2">
|
||||||
|
{secretTags.map(({ id, slug }, i) => (
|
||||||
|
<Tag
|
||||||
|
className={cx(
|
||||||
|
tagColors[i % tagColors.length].bg,
|
||||||
|
tagColors[i % tagColors.length].text
|
||||||
|
)}
|
||||||
|
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||||
|
onClose={() => remove(i)}
|
||||||
|
key={id}
|
||||||
|
>
|
||||||
|
{slug}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
{!(isReadOnly || isAddOnly || isRollbackMode) && (
|
||||||
|
<div className="w-0 overflow-hidden group-hover:w-8">
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<Tooltip content="Add tags">
|
||||||
|
<IconButton variant="star" size="xs" ariaLabel="add-tag" className="py-[0.42rem]">
|
||||||
|
<FontAwesomeIcon icon={faTags} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
side="left"
|
||||||
|
className="max-h-96 w-auto min-w-[200px] overflow-y-auto overflow-x-hidden p-2 text-bunker-200 bg-mineshaft-800 border border-mineshaft-600"
|
||||||
|
hideCloseBtn
|
||||||
|
>
|
||||||
|
<div className="mb-2 text-sm font-medium text-center text-bunker-200 px-2">Add tags to {secret.key || "this secret"}</div>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
{wsTags?.map((wsTag) => (
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className={twMerge(
|
||||||
|
'justify-start bg-mineshaft-600 text-bunker-100 hover:bg-mineshaft-500',
|
||||||
|
selectedTagIds?.[wsTag.slug] && 'text-primary'
|
||||||
|
)}
|
||||||
|
onClick={() => onSelectTag(wsTag)}
|
||||||
|
leftIcon={
|
||||||
|
<Checkbox
|
||||||
|
className="data-[state=checked]:bg-primary mr-0"
|
||||||
|
id="autoCapitalization"
|
||||||
|
isChecked={selectedTagIds?.[wsTag.slug]}
|
||||||
|
onCheckedChange={() => {}}
|
||||||
|
>
|
||||||
|
{}
|
||||||
|
</Checkbox>
|
||||||
|
}
|
||||||
|
key={wsTag._id}
|
||||||
|
>
|
||||||
|
{wsTag.slug}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="star"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
className="mt-4 justify-start bg-mineshaft-600 h-7 px-1"
|
||||||
|
onClick={onCreateTagOpen}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add new tag
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-0 group-hover:w-14 invisible group-hover:visible duration-0 items-center justify-end space-x-2 overflow-hidden transition-all">
|
||||||
|
{!isAddOnly && (
|
||||||
|
<div>
|
||||||
|
<Tooltip content="Settings">
|
||||||
|
<IconButton size="lg" colorSchema="primary" variant="plain" onClick={onRowExpand} ariaLabel="expand">
|
||||||
|
<FontAwesomeIcon icon={faEllipsis} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Tooltip content="Delete">
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="delete"
|
||||||
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
|
onClick={() => onSecretDelete(index, secret._id, secret?.idOverride)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretInputRow } from './SecretInputRow';
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Controller } from 'react-hook-form';
|
||||||
|
import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import { IconButton } from '@app/components/v2';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sortDir: 'asc' | 'desc';
|
||||||
|
onSort: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretTableHeader = ({
|
||||||
|
sortDir,
|
||||||
|
onSort
|
||||||
|
}: Props): JSX.Element => (
|
||||||
|
<thead>
|
||||||
|
<tr className="absolute flex flex-row sticky top-0">
|
||||||
|
<td className="w-10 px-4 flex items-center justify-center">
|
||||||
|
<div className='text-center w-10 text-xs text-transparent'>{0}</div>
|
||||||
|
</td>
|
||||||
|
<Controller
|
||||||
|
defaultValue=""
|
||||||
|
name="na"
|
||||||
|
render={() => (
|
||||||
|
<td className='flex items-center'>
|
||||||
|
<div className="min-w-[220px] lg:min-w-[240px] xl:min-w-[280px] relative flex items-center justify-start pl-2.5 w-full">
|
||||||
|
<div className="inline-flex items-end text-md font-medium">
|
||||||
|
Key
|
||||||
|
<IconButton variant="plain" className="ml-2" ariaLabel="sort" onClick={onSort}>
|
||||||
|
<FontAwesomeIcon icon={sortDir === 'asc' ? faArrowDown : faArrowUp} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div className="w-max flex flex-row items-center justify-end">
|
||||||
|
<div className="w-5 overflow-hidden group-hover:w-5 mt-1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<th className="flex flex-row w-full"><div className="text-sm font-medium">Value</div></th>
|
||||||
|
</tr>
|
||||||
|
<tr className='h-0 w-full border border-mineshaft-600'/>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretTableHeader } from './SecretTableHeader';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { DashboardPage } from './DashboardPage';
|
||||||
@@ -1471,6 +1471,22 @@ module.exports = {
|
|||||||
opacity: 1
|
opacity: 1
|
||||||
// transform: "translateY(100%)",
|
// transform: "translateY(100%)",
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
drawerRightIn: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateX(100%)'
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateX(0)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawerRightOut: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateX(0)'
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateX(100%)'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
@@ -1478,6 +1494,9 @@ module.exports = {
|
|||||||
// MODAL
|
// MODAL
|
||||||
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
|
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
|
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
|
||||||
|
// drawer
|
||||||
|
drawerRightIn: 'drawerRightIn 150ms ease-in-out',
|
||||||
|
drawerRightOut: 'drawerRightOut 150ms ease-in-out',
|
||||||
// Dropdown
|
// Dropdown
|
||||||
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
|||||||
Reference in New Issue
Block a user