mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 06:25:55 +00:00
feat: implemented new overview page with improvement in dashboard
This commit is contained in:
Generated
+322
-12
@@ -67,6 +67,7 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-code-input": "^3.10.1",
|
"react-code-input": "^3.10.1",
|
||||||
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-hook-form": "^7.43.0",
|
"react-hook-form": "^7.43.0",
|
||||||
@@ -75,6 +76,7 @@
|
|||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"sanitize-html": "^2.11.0",
|
||||||
"set-cookie-parser": "^2.5.1",
|
"set-cookie-parser": "^2.5.1",
|
||||||
"sharp": "^0.32.0",
|
"sharp": "^0.32.0",
|
||||||
"styled-components": "^5.3.7",
|
"styled-components": "^5.3.7",
|
||||||
@@ -99,6 +101,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.9",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
@@ -7922,6 +7925,89 @@
|
|||||||
"redux": "^4.0.0"
|
"redux": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sanitize-html": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"htmlparser2": "^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/sanitize-html/node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/scheduler": {
|
"node_modules/@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
@@ -10745,7 +10831,6 @@
|
|||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -11203,7 +11288,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -12690,8 +12774,7 @@
|
|||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fast-diff": {
|
"node_modules/fast-diff": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@@ -17860,6 +17943,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -18113,7 +18201,6 @@
|
|||||||
"version": "8.4.23",
|
"version": "8.4.23",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
||||||
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -18936,6 +19023,18 @@
|
|||||||
"react-dom": ">=16.8.0"
|
"react-dom": ">=16.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-contenteditable": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"prop-types": "^15.7.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-docgen": {
|
"node_modules/react-docgen": {
|
||||||
"version": "5.4.3",
|
"version": "5.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
||||||
@@ -20017,6 +20116,88 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/sanitize-html": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^8.0.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sass-loader": {
|
"node_modules/sass-loader": {
|
||||||
"version": "12.6.0",
|
"version": "12.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||||
@@ -28425,6 +28606,66 @@
|
|||||||
"redux": "^4.0.0"
|
"redux": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/sanitize-html": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"htmlparser2": "^8.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/scheduler": {
|
"@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
@@ -30633,8 +30874,7 @@
|
|||||||
"deepmerge": {
|
"deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"default-browser": {
|
"default-browser": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -30963,8 +31203,7 @@
|
|||||||
"domelementtype": {
|
"domelementtype": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"domhandler": {
|
"domhandler": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
@@ -32126,8 +32365,7 @@
|
|||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"fast-diff": {
|
"fast-diff": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@@ -35865,6 +36103,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"parse-srcset": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
|
||||||
|
},
|
||||||
"parseurl": {
|
"parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -36081,7 +36324,6 @@
|
|||||||
"version": "8.4.23",
|
"version": "8.4.23",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
|
||||||
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.6",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
@@ -36653,6 +36895,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"react-contenteditable": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==",
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"prop-types": "^15.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-docgen": {
|
"react-docgen": {
|
||||||
"version": "5.4.3",
|
"version": "5.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
||||||
@@ -37421,6 +37672,65 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"sanitize-html": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
|
||||||
|
"requires": {
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"htmlparser2": "^8.0.0",
|
||||||
|
"is-plain-object": "^5.0.0",
|
||||||
|
"parse-srcset": "^1.0.2",
|
||||||
|
"postcss": "^8.3.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"domutils": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||||
|
"requires": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||||
|
},
|
||||||
|
"htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"requires": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sass-loader": {
|
"sass-loader": {
|
||||||
"version": "12.6.0",
|
"version": "12.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-code-input": "^3.10.1",
|
"react-code-input": "^3.10.1",
|
||||||
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-hook-form": "^7.43.0",
|
"react-hook-form": "^7.43.0",
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"sanitize-html": "^2.11.0",
|
||||||
"set-cookie-parser": "^2.5.1",
|
"set-cookie-parser": "^2.5.1",
|
||||||
"sharp": "^0.32.0",
|
"sharp": "^0.32.0",
|
||||||
"styled-components": "^5.3.7",
|
"styled-components": "^5.3.7",
|
||||||
@@ -107,6 +109,7 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.9",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
|||||||
@@ -13,6 +13,11 @@
|
|||||||
.flex-3 {
|
.flex-3 {
|
||||||
flex-grow: 3;
|
flex-grow: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-table-row {
|
||||||
|
width: 1%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@@ -40,7 +45,7 @@
|
|||||||
|
|
||||||
.breadcrumb::after,
|
.breadcrumb::after,
|
||||||
.breadcrumb::before {
|
.breadcrumb::before {
|
||||||
content: '';
|
content: "";
|
||||||
height: 60%;
|
height: 60%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
@@ -58,18 +63,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb::after {
|
.breadcrumb::after {
|
||||||
left: 5px;
|
left: 4px;
|
||||||
bottom: -3px;
|
bottom: -2.5px;
|
||||||
transform: skew(-30deg);
|
transform: skew(-30deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumb::before {
|
.breadcrumb::before {
|
||||||
left: 5px;
|
left: 4px;
|
||||||
top: -3px;
|
top: -2.5px;
|
||||||
transform: skew(30deg);
|
transform: skew(30deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 0.25rem;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: gray transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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";
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { faFolderOpen, faKey, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
|
||||||
import { Button, Input, TableContainer, Tooltip } from "@app/components/v2";
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
|
||||||
import {
|
|
||||||
useGetProjectFoldersBatch,
|
|
||||||
useGetProjectSecretsByKey,
|
|
||||||
useGetUserWsEnvironments,
|
|
||||||
useGetUserWsKey
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
|
|
||||||
import { EnvComparisonRow } from "./components/EnvComparisonRow";
|
|
||||||
import { FolderComparisonRow } from "./components/EnvComparisonRow/FolderComparisonRow";
|
|
||||||
|
|
||||||
export const DashboardEnvOverview = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { currentWorkspace, isLoading } = useWorkspace();
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const workspaceId = currentWorkspace?._id as string;
|
|
||||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
|
||||||
const secretPath = router.query?.secretPath as string;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && !workspaceId && router.isReady) {
|
|
||||||
router.push(`/org/${currentOrg?._id}/overview`);
|
|
||||||
}
|
|
||||||
}, [isLoading, workspaceId, router.isReady]);
|
|
||||||
|
|
||||||
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
|
||||||
workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied);
|
|
||||||
|
|
||||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecretsByKey({
|
|
||||||
workspaceId,
|
|
||||||
env: userAvailableEnvs?.map((env) => env.slug) ?? [],
|
|
||||||
decryptFileKey: latestFileKey!,
|
|
||||||
isPaused: false,
|
|
||||||
secretPath
|
|
||||||
});
|
|
||||||
|
|
||||||
const folders = useGetProjectFoldersBatch({
|
|
||||||
folders:
|
|
||||||
userAvailableEnvs?.map((env) => ({
|
|
||||||
environment: env.slug,
|
|
||||||
workspaceId
|
|
||||||
})) ?? [],
|
|
||||||
parentFolderPath: secretPath
|
|
||||||
});
|
|
||||||
|
|
||||||
const foldersGroupedByEnv = useMemo(() => {
|
|
||||||
const res: Record<string, Record<string, boolean>> = {};
|
|
||||||
folders.forEach(({ data }) => {
|
|
||||||
data?.folders
|
|
||||||
?.filter(({ name }) => name.toLowerCase().includes(searchFilter))
|
|
||||||
?.forEach((folder) => {
|
|
||||||
if (!res?.[folder.name]) res[folder.name] = {};
|
|
||||||
res[folder.name][data.environment] = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
}, [folders, userAvailableEnvs, searchFilter]);
|
|
||||||
|
|
||||||
const numSecretsMissingPerEnv = useMemo(() => {
|
|
||||||
// first get all sec in the env then subtract with total to get missing ones
|
|
||||||
const secPerEnvMissing: Record<string, number> = Object.fromEntries(
|
|
||||||
(userAvailableEnvs || [])?.map(({ slug }) => [slug, 0])
|
|
||||||
);
|
|
||||||
Object.keys(secrets?.secrets || {}).forEach((key) =>
|
|
||||||
secrets?.secrets?.[key].forEach((val) => {
|
|
||||||
secPerEnvMissing[val.env] += 1;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
Object.keys(secPerEnvMissing).forEach((k) => {
|
|
||||||
secPerEnvMissing[k] = (secrets?.uniqueSecCount || 0) - secPerEnvMissing[k];
|
|
||||||
});
|
|
||||||
return secPerEnvMissing;
|
|
||||||
}, [secrets, userAvailableEnvs]);
|
|
||||||
|
|
||||||
const onExploreEnv = (slug: string) => {
|
|
||||||
const query: Record<string, string> = { ...router.query, env: slug };
|
|
||||||
delete query.secretPath;
|
|
||||||
// the dir return will have the present directory folder id
|
|
||||||
// use that when clicking on explore to redirect user to there
|
|
||||||
const envFolder = folders.find(({ data }) => slug === data?.environment);
|
|
||||||
const dir = envFolder?.data?.dir?.pop();
|
|
||||||
if (dir) {
|
|
||||||
query.folderId = dir.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFolderClick = (path: string) => {
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query: {
|
|
||||||
...router.query,
|
|
||||||
secretPath: `${router.query?.secretPath || ""}/${path}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFolderCrumbClick = (index: number) => {
|
|
||||||
const newSecPath = secretPath.split("/").filter(Boolean).slice(0, index).join("/");
|
|
||||||
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
|
|
||||||
// root condition
|
|
||||||
if (index === 0) delete query.secretPath;
|
|
||||||
router.push({
|
|
||||||
pathname: router.pathname,
|
|
||||||
query
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isSecretsLoading || isEnvListLoading) {
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto flex h-screen 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredSecrets = Object.keys(secrets?.secrets || {})?.filter((secret: any) =>
|
|
||||||
secret.toUpperCase().includes(searchFilter.toUpperCase())
|
|
||||||
);
|
|
||||||
// when secrets is not loading and secrets list is empty
|
|
||||||
const isDashboardSecretEmpty = !isSecretsLoading && !filteredSecrets?.length;
|
|
||||||
const isFoldersEmtpy =
|
|
||||||
!folders.some(({ isLoading: isFolderLoading }) => isFolderLoading) &&
|
|
||||||
!Object.keys(foldersGroupedByEnv).length;
|
|
||||||
const isDashboardEmpty = isFoldersEmtpy && isDashboardSecretEmpty;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
|
||||||
<div className="relative right-5 ml-4">
|
|
||||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
|
||||||
</div>
|
|
||||||
<div className="mt-6">
|
|
||||||
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
|
||||||
<p className="text-md text-bunker-300">
|
|
||||||
Inject your secrets using
|
|
||||||
<a
|
|
||||||
className="mx-1 text-primary/80 hover:text-primary"
|
|
||||||
href="https://infisical.com/docs/cli/overview"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Infisical CLI
|
|
||||||
</a>
|
|
||||||
or
|
|
||||||
<a
|
|
||||||
className="mx-1 text-primary/80 hover:text-primary"
|
|
||||||
href="https://infisical.com/docs/sdks/overview"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Infisical SDKs
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<div
|
|
||||||
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-600 py-1 pl-5 pr-2 text-sm"
|
|
||||||
onClick={() => onFolderCrumbClick(0)}
|
|
||||||
onKeyDown={() => null}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
|
|
||||||
</div>
|
|
||||||
{(secretPath || "")
|
|
||||||
.split("/")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((path, index, arr) => (
|
|
||||||
<div
|
|
||||||
key={`secret-path-${index + 1}`}
|
|
||||||
className={`breadcrumb relative z-20 ${
|
|
||||||
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
|
|
||||||
} border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
|
|
||||||
onClick={() => onFolderCrumbClick(index + 1)}
|
|
||||||
onKeyDown={() => null}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{path}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="w-80">
|
|
||||||
<Input
|
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
|
||||||
placeholder="Search by secret/folder name..."
|
|
||||||
value={searchFilter}
|
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-y-auto">
|
|
||||||
<div className="sticky top-0 mt-3 flex h-10 min-w-[60.3rem] flex-row rounded-md border border-mineshaft-600 bg-mineshaft-800">
|
|
||||||
<div className="sticky top-0 flex w-10 items-center justify-center border-none px-4">
|
|
||||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
|
||||||
</div>
|
|
||||||
<div className="sticky top-0 border-none">
|
|
||||||
<div className="relative flex h-full w-full min-w-[200px] items-center justify-start lg:min-w-[220px] xl:min-w-[250px]">
|
|
||||||
<div className="text-sm font-medium ">Secret</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{numSecretsMissingPerEnv &&
|
|
||||||
userAvailableEnvs?.map((env) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`header-${env.slug}`}
|
|
||||||
className="flex w-full min-w-[11rem] flex-row items-center rounded-md border-none bg-mineshaft-800"
|
|
||||||
>
|
|
||||||
<div className="flex w-full flex-row justify-center text-center text-sm font-medium text-bunker-200/[.99]">
|
|
||||||
{env.name}
|
|
||||||
{numSecretsMissingPerEnv[env.slug] > 0 && (
|
|
||||||
<div className="mt-0.5 ml-2.5 flex h-[1.1rem] w-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red text-xs text-bunker-100">
|
|
||||||
<Tooltip
|
|
||||||
content={`${
|
|
||||||
numSecretsMissingPerEnv[env.slug]
|
|
||||||
} secrets missing compared to other environments`}
|
|
||||||
>
|
|
||||||
<span className="text-bunker-100">
|
|
||||||
{numSecretsMissingPerEnv[env.slug]}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isDashboardEmpty ? "" : ""
|
|
||||||
} no-scrollbar::-webkit-scrollbar mt-3 flex h-full max-h-[calc(100vh-370px)] w-full min-w-[60.3rem] flex-grow flex-row items-start justify-center overflow-x-hidden rounded-md border border-mineshaft-600 no-scrollbar`}
|
|
||||||
>
|
|
||||||
{!isDashboardEmpty && (
|
|
||||||
<TableContainer className="border-none">
|
|
||||||
<table className="secret-table relative w-full bg-mineshaft-900">
|
|
||||||
<tbody className="max-h-screen overflow-y-auto">
|
|
||||||
{Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => (
|
|
||||||
<FolderComparisonRow
|
|
||||||
key={`${folderName}-${index + 1}`}
|
|
||||||
folderName={folderName}
|
|
||||||
userAvailableEnvs={userAvailableEnvs}
|
|
||||||
folderInEnv={foldersGroupedByEnv[folderName]}
|
|
||||||
onClick={onFolderClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{Object.keys(secrets?.secrets || {})
|
|
||||||
?.filter((secret: any) =>
|
|
||||||
secret.toUpperCase().includes(searchFilter.toUpperCase())
|
|
||||||
)
|
|
||||||
.map((key) => (
|
|
||||||
<EnvComparisonRow
|
|
||||||
key={`row-${key}`}
|
|
||||||
secrets={secrets?.secrets?.[key]}
|
|
||||||
isReadOnly
|
|
||||||
isSecretValueHidden
|
|
||||||
userAvailableEnvs={userAvailableEnvs}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</TableContainer>
|
|
||||||
)}
|
|
||||||
{isDashboardEmpty && (
|
|
||||||
<div className="flex h-40 w-full flex-row rounded-md">
|
|
||||||
<div className="flex w-full min-w-[11rem] flex-col items-center justify-center rounded-md border-none bg-mineshaft-800 text-bunker-300">
|
|
||||||
<FontAwesomeIcon icon={faKey} className="mb-4 text-4xl" />
|
|
||||||
<span className="mb-1">No secrets/folders found.</span>
|
|
||||||
<span>To add more secrets you can explore any environment.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="group mt-4 flex min-w-[60.3rem] flex-row items-center">
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center border-none px-4">
|
|
||||||
<div className="w-10 text-center text-xs text-transparent">0</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
|
||||||
<span className="text-transparent">0</span>
|
|
||||||
<button type="button" className="mr-2 text-transparent">
|
|
||||||
1
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{userAvailableEnvs?.map((env) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`button-${env.slug}`}
|
|
||||||
className="mx-2 mb-1 flex h-10 w-full min-w-[11rem] flex-row items-center justify-center border-none"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={() => onExploreEnv(env.slug)}
|
|
||||||
variant="outline_bg"
|
|
||||||
colorSchema="primary"
|
|
||||||
isFullWidth
|
|
||||||
className="h-10"
|
|
||||||
>
|
|
||||||
Explore {env.name}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -30,7 +30,11 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@radix-ui/react-dropdown-menu";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
@@ -119,12 +123,13 @@ type TDeleteSecretImport = { environment: string; secretPath: string };
|
|||||||
* Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
|
* 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
|
* They will get it back
|
||||||
*/
|
*/
|
||||||
export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
export const DashboardPage = () => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const envQuery = router.query.env as string;
|
||||||
|
|
||||||
const secretContainer = useRef<HTMLDivElement | null>(null);
|
const secretContainer = useRef<HTMLDivElement | null>(null);
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
@@ -172,8 +177,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
// get an env with one of the access available
|
// get an env with one of the access available
|
||||||
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
|
||||||
if (env && data?.map((wsenv) => wsenv.slug).includes(envFromTop)) {
|
if (env && data?.map((wsenv) => wsenv.slug).includes(envQuery)) {
|
||||||
setSelectedEnv(data?.filter((dp) => dp.slug === envFromTop)[0]);
|
setSelectedEnv(data?.filter((dp) => dp.slug === envQuery)[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -293,11 +298,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
register,
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
getValues,
|
getValues,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { isSubmitting, isDirty },
|
formState: { isSubmitting, isDirty, errors },
|
||||||
reset
|
reset
|
||||||
} = method;
|
} = method;
|
||||||
const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" });
|
const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" });
|
||||||
@@ -461,9 +467,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDrawerOpen = (dto: TSecretDetailsOpen) => {
|
const onDrawerOpen = useCallback((id: string | undefined, index: number) => {
|
||||||
handlePopUpOpen("secretDetails", dto);
|
handlePopUpOpen("secretDetails", { id, index } as TSecretDetailsOpen);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onEnvChange = (slug: string) => {
|
const onEnvChange = (slug: string) => {
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
@@ -480,17 +486,27 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadSecret = () => {
|
||||||
|
const secretsFromImport: { key: string; value: string; comment: string }[] = [];
|
||||||
|
importedSecrets?.forEach(({ secrets: impSec }) => {
|
||||||
|
impSec.forEach((el) => {
|
||||||
|
secretsFromImport.push({ key: el.key, value: el.value, comment: el.comment });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
downloadSecret(getValues("secrets"), secretsFromImport, selectedEnv?.slug);
|
||||||
|
};
|
||||||
|
|
||||||
// record all deleted ids
|
// record all deleted ids
|
||||||
// This will make final deletion easier
|
// This will make final deletion easier
|
||||||
const onSecretDelete = (index: number, id?: string, overrideId?: string) => {
|
const onSecretDelete = useCallback((index: number, id?: string, overrideId?: string) => {
|
||||||
if (id) deletedSecretIds.current.push(id);
|
if (id) deletedSecretIds.current.push(id);
|
||||||
if (overrideId) deletedSecretIds.current.push(overrideId);
|
if (overrideId) deletedSecretIds.current.push(overrideId);
|
||||||
remove(index);
|
remove(index);
|
||||||
// just the case if this is called from drawer
|
// just the case if this is called from drawer
|
||||||
handlePopUpClose("secretDetails");
|
handlePopUpClose("secretDetails");
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onCreateWsTag = async (tagName: string) => {
|
const onCreateWsTag = useCallback(async (tagName: string) => {
|
||||||
try {
|
try {
|
||||||
await createWsTag({
|
await createWsTag({
|
||||||
workspaceID: workspaceId,
|
workspaceID: workspaceId,
|
||||||
@@ -509,10 +525,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleFolderOpen = (id: string) => {
|
const handleFolderOpen = useCallback((id: string) => {
|
||||||
setSearchFilter("");
|
setSearchFilter("");
|
||||||
|
console.log(router.query);
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: {
|
query: {
|
||||||
@@ -520,10 +537,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
folderId: id
|
folderId: id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const isEditFolder = Boolean(popUp?.folderForm?.data);
|
const isEditFolder = Boolean(popUp?.folderForm?.data);
|
||||||
|
|
||||||
|
// FOLDER SECTION
|
||||||
const handleFolderCreate = async (name: string) => {
|
const handleFolderCreate = async (name: string) => {
|
||||||
try {
|
try {
|
||||||
await createFolder({
|
await createFolder({
|
||||||
@@ -546,7 +564,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFolderUpdate = async (name: string) => {
|
const handleFolderUpdate = useCallback(async (name: string) => {
|
||||||
const { id } = popUp?.folderForm?.data as TDeleteFolderForm;
|
const { id } = popUp?.folderForm?.data as TDeleteFolderForm;
|
||||||
try {
|
try {
|
||||||
await updateFolder({
|
await updateFolder({
|
||||||
@@ -567,9 +585,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleFolderDelete = async () => {
|
const handleFolderDelete = useCallback(async () => {
|
||||||
const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm;
|
const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm;
|
||||||
try {
|
try {
|
||||||
deleteFolder({
|
deleteFolder({
|
||||||
@@ -589,8 +607,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
// SECRET IMPORT SECTION
|
||||||
const handleSecretImportCreate = async (env: string, secretPath: string) => {
|
const handleSecretImportCreate = async (env: string, secretPath: string) => {
|
||||||
try {
|
try {
|
||||||
await createSecretImport({
|
await createSecretImport({
|
||||||
@@ -664,6 +683,25 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// OPTIMIZATION HOOKS PURELY FOR PERFORMANCE AND TO AVOID RE-RENDERING
|
||||||
|
const handleCreateTagModalOpen = useCallback(() => handlePopUpOpen("addTag"), []);
|
||||||
|
const handleFolderCreatePopUpOpen = useCallback(
|
||||||
|
(id: string, name: string) => handlePopUpOpen("folderForm", { id, name }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const handleFolderDeletePopUpOpen = useCallback(
|
||||||
|
(id: string, name: string) => handlePopUpOpen("deleteFolder", { id, name }),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const handleSecretImportDelPopUpOpen = useCallback(
|
||||||
|
(impSecEnv: string, impSecPath: string) =>
|
||||||
|
handlePopUpOpen("deleteSecretImport", {
|
||||||
|
environment: impSecEnv,
|
||||||
|
secretPath: impSecPath
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// when secrets is not loading and secrets list is empty
|
// when secrets is not loading and secrets list is empty
|
||||||
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
||||||
|
|
||||||
@@ -693,259 +731,259 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<FormProvider {...method}>
|
<form autoComplete="off" className="h-full">
|
||||||
<form autoComplete="off" className="h-full">
|
{/* breadcrumb row */}
|
||||||
{/* breadcrumb row */}
|
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||||
<div className="relative right-6 -top-2 mb-2 ml-6">
|
<NavHeader
|
||||||
<NavHeader
|
pageName={t("dashboard.title")}
|
||||||
pageName={t("dashboard.title")}
|
currentEnv={userAvailableEnvs?.filter((envir) => envir.slug === envQuery)[0].name || ""}
|
||||||
currentEnv={
|
isFolderMode
|
||||||
userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || ""
|
folders={folderData?.dir}
|
||||||
}
|
isProjectRelated
|
||||||
isFolderMode
|
userAvailableEnvs={userAvailableEnvs}
|
||||||
folders={folderData?.dir}
|
onEnvChange={onEnvChange}
|
||||||
isProjectRelated
|
/>
|
||||||
userAvailableEnvs={userAvailableEnvs}
|
</div>
|
||||||
onEnvChange={onEnvChange}
|
<div className="mb-4">
|
||||||
|
<h6 className="text-2xl">{isRollbackMode ? "Secret Snapshot" : ""}</h6>
|
||||||
|
{isRollbackMode && Boolean(snapshotSecret) && (
|
||||||
|
<Tag colorSchema="green">
|
||||||
|
{new Date(snapshotSecret?.createdAt || "").toLocaleString()}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Environment, search and other action row */}
|
||||||
|
<div className="flex items-center justify-between space-x-2">
|
||||||
|
<div className="flex max-w-lg flex-grow space-x-2">
|
||||||
|
<Input
|
||||||
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
|
placeholder="Search by folder name, key name, comment..."
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="flex items-center space-x-2">
|
||||||
<h6 className="text-2xl">{isRollbackMode ? "Secret Snapshot" : ""}</h6>
|
<div>
|
||||||
{isRollbackMode && Boolean(snapshotSecret) && (
|
<Popover>
|
||||||
<Tag colorSchema="green">
|
<PopoverTrigger asChild>
|
||||||
{new Date(snapshotSecret?.createdAt || "").toLocaleString()}
|
<IconButton ariaLabel="download" variant="outline_bg">
|
||||||
</Tag>
|
<FontAwesomeIcon icon={faDownload} />
|
||||||
)}
|
</IconButton>
|
||||||
</div>
|
</PopoverTrigger>
|
||||||
{/* Environment, search and other action row */}
|
<PopoverContent
|
||||||
<div className="flex items-center justify-between space-x-2">
|
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-1"
|
||||||
<div className="flex max-w-lg flex-grow space-x-2">
|
hideCloseBtn
|
||||||
<Input
|
>
|
||||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
<div className="flex flex-col space-y-2">
|
||||||
placeholder="Search by folder name, key name, comment..."
|
<Button
|
||||||
value={searchFilter}
|
onClick={handleDownloadSecret}
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
colorSchema="primary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
variant="outline_bg"
|
||||||
/>
|
className="h-8 bg-bunker-700"
|
||||||
|
>
|
||||||
|
Download as .env
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div>
|
||||||
<div>
|
<Tooltip content={isSecretValueHidden ? "Reveal Secrets" : "Hide secrets"}>
|
||||||
<Popover>
|
<IconButton
|
||||||
<PopoverTrigger asChild>
|
ariaLabel="reveal"
|
||||||
<IconButton ariaLabel="download" variant="outline_bg">
|
|
||||||
<FontAwesomeIcon icon={faDownload} />
|
|
||||||
</IconButton>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-1"
|
|
||||||
hideCloseBtn
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => downloadSecret(getValues("secrets"), selectedEnv?.slug)}
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
className="h-8 bg-bunker-700"
|
|
||||||
>
|
|
||||||
Download as .env
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Tooltip content={isSecretValueHidden ? "Reveal Secrets" : "Hide secrets"}>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="reveal"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => setIsSecretValueHidden.toggle()}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isSecretValueHidden ? faEye : faEyeSlash} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="block xl:hidden">
|
|
||||||
<Tooltip content="Point-in-time Recovery">
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="recovery"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => handlePopUpOpen("secretSnapshots")}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faCodeCommit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className="hidden xl:block">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => {
|
onClick={() => setIsSecretValueHidden.toggle()}
|
||||||
if (subscription && subscription.pitRecovery) {
|
|
||||||
handlePopUpOpen("secretSnapshots");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
|
||||||
isLoading={isLoadingSnapshotCount}
|
|
||||||
isDisabled={!canDoRollback}
|
|
||||||
className="h-10"
|
|
||||||
>
|
>
|
||||||
{snapshotCount} Commits
|
<FontAwesomeIcon icon={isSecretValueHidden ? faEye : faEyeSlash} />
|
||||||
</Button>
|
</IconButton>
|
||||||
</div>
|
</Tooltip>
|
||||||
{!isReadOnly && !isRollbackMode && (
|
</div>
|
||||||
<div className="flex flex-row items-center justify-center">
|
<div className="block xl:hidden">
|
||||||
<button
|
<Tooltip content="Point-in-time Recovery">
|
||||||
type="button"
|
<IconButton
|
||||||
onClick={() => {
|
ariaLabel="recovery"
|
||||||
if (!(isReadOnly || isRollbackMode)) {
|
variant="outline_bg"
|
||||||
if (secretContainer.current) {
|
onClick={() => handlePopUpOpen("secretSnapshots")}
|
||||||
secretContainer.current.scroll({
|
|
||||||
top: 0,
|
|
||||||
behavior: "smooth"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
|
||||||
setSearchFilter("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="font-semibold bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-l-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 pr-4 duration-200"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
|
|
||||||
</button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
|
||||||
<div className="bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-r-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 duration-200">
|
|
||||||
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5"/>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="mt-1 z-[60] left-20 w-[10.8rem]">
|
|
||||||
<div className="bg-mineshaft-800 p-1 border border-mineshaft-600 rounded-md">
|
|
||||||
<div className="w-full pb-1">
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
|
||||||
onClick={() => handlePopUpOpen("folderForm")}
|
|
||||||
isDisabled={isReadOnly || isRollbackMode}
|
|
||||||
variant="outline_bg"
|
|
||||||
className="h-10"
|
|
||||||
isFullWidth
|
|
||||||
>
|
|
||||||
Add Folder
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
|
||||||
onClick={() => handlePopUpOpen("addSecretImport")}
|
|
||||||
isDisabled={isReadOnly || isRollbackMode}
|
|
||||||
variant="outline_bg"
|
|
||||||
className="h-10"
|
|
||||||
isFullWidth
|
|
||||||
>
|
|
||||||
Add Import
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isRollbackMode && (
|
|
||||||
<Button
|
|
||||||
variant="star"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
|
||||||
onClick={() => {
|
|
||||||
setSnaphotId(null);
|
|
||||||
reset({ ...secrets, isSnapshotMode: false });
|
|
||||||
}}
|
|
||||||
className="h-10"
|
|
||||||
>
|
>
|
||||||
Go back
|
<FontAwesomeIcon icon={faCodeCommit} />
|
||||||
</Button>
|
</IconButton>
|
||||||
)}
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="hidden xl:block">
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitDisabled}
|
variant="outline_bg"
|
||||||
isLoading={isSubmitting}
|
onClick={() => {
|
||||||
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
if (subscription && subscription.pitRecovery) {
|
||||||
onClick={handleSubmit(onSaveSecret)}
|
handlePopUpOpen("secretSnapshots");
|
||||||
className="h-10 text-black"
|
return;
|
||||||
color="primary"
|
}
|
||||||
variant="solid"
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||||
|
isLoading={isLoadingSnapshotCount}
|
||||||
|
isDisabled={!canDoRollback}
|
||||||
|
className="h-10"
|
||||||
>
|
>
|
||||||
{isRollbackMode ? "Rollback" : "Save Changes"}
|
{snapshotCount} Commits
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{!isReadOnly && !isRollbackMode && (
|
||||||
<div
|
<div className="flex flex-row items-center justify-center">
|
||||||
className={`${
|
<button
|
||||||
isEmptyPage ? "flex flex-col items-center justify-center" : ""
|
type="button"
|
||||||
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
onClick={() => {
|
||||||
ref={secretContainer}
|
if (!(isReadOnly || isRollbackMode)) {
|
||||||
>
|
if (secretContainer.current) {
|
||||||
{!isEmptyPage && (
|
secretContainer.current.scroll({
|
||||||
<DndContext
|
top: 0,
|
||||||
onDragEnd={handleDragEnd}
|
behavior: "smooth"
|
||||||
sensors={sensors}
|
});
|
||||||
collisionDetection={closestCenter}
|
}
|
||||||
modifiers={[restrictToVerticalAxis]}
|
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||||
>
|
setSearchFilter("");
|
||||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
}
|
||||||
<table className="secret-table relative">
|
}}
|
||||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
className="cursor-pointer rounded-l-md border border-mineshaft-500 bg-mineshaft-600 p-2 pr-4 text-sm font-semibold text-mineshaft-300 duration-200 hover:border-primary/40 hover:bg-primary/[0.1]"
|
||||||
<tbody className="max-h-96 overflow-y-auto">
|
>
|
||||||
<SecretImportSection
|
<FontAwesomeIcon icon={faPlus} className="px-2" />
|
||||||
onSecretImportDelete={(impSecEnv, impSecPath) =>
|
Add Secret
|
||||||
handlePopUpOpen("deleteSecretImport", {
|
</button>
|
||||||
environment: impSecEnv,
|
<DropdownMenu>
|
||||||
secretPath: impSecPath
|
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||||
})
|
<div className="cursor-pointer rounded-r-md border border-mineshaft-500 bg-mineshaft-600 p-2 text-sm text-mineshaft-300 duration-200 hover:border-primary/40 hover:bg-primary/[0.1]">
|
||||||
}
|
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5" />
|
||||||
secrets={secrets?.secrets}
|
</div>
|
||||||
importedSecrets={importedSecrets}
|
</DropdownMenuTrigger>
|
||||||
items={items}
|
<DropdownMenuContent align="end" className="left-20 z-[60] mt-1 w-[10.8rem]">
|
||||||
/>
|
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||||
<FolderSection
|
<div className="w-full pb-1">
|
||||||
onFolderOpen={handleFolderOpen}
|
<Button
|
||||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
onClick={() => handlePopUpOpen("folderForm")}
|
||||||
folders={folderList}
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
search={searchFilter}
|
variant="outline_bg"
|
||||||
/>
|
className="h-10"
|
||||||
{fields.map(({ id, _id }, index) => (
|
isFullWidth
|
||||||
<SecretInputRow
|
>
|
||||||
key={id}
|
Add Folder
|
||||||
isReadOnly={isReadOnly}
|
</Button>
|
||||||
isRollbackMode={isRollbackMode}
|
</div>
|
||||||
isAddOnly={isAddOnly}
|
<div className="w-full">
|
||||||
index={index}
|
<Button
|
||||||
searchTerm={searchFilter}
|
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||||
onSecretDelete={onSecretDelete}
|
onClick={() => handlePopUpOpen("addSecretImport")}
|
||||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
isSecretValueHidden={isSecretValueHidden}
|
variant="outline_bg"
|
||||||
wsTags={wsTags}
|
className="h-10"
|
||||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
isFullWidth
|
||||||
/>
|
>
|
||||||
))}
|
Add Import
|
||||||
{!isReadOnly && !isRollbackMode && (
|
</Button>
|
||||||
<tr>
|
</div>
|
||||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
</div>
|
||||||
<button
|
</DropdownMenuContent>
|
||||||
type="button"
|
</DropdownMenu>
|
||||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
</div>
|
||||||
onClick={onAppendSecret}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
|
||||||
<span className="ml-2 w-20">Add Secret</span>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</TableContainer>
|
|
||||||
</DndContext>
|
|
||||||
)}
|
)}
|
||||||
|
{isRollbackMode && (
|
||||||
|
<Button
|
||||||
|
variant="star"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||||
|
onClick={() => {
|
||||||
|
setSnaphotId(null);
|
||||||
|
reset({ ...secrets, isSnapshotMode: false });
|
||||||
|
}}
|
||||||
|
className="h-10"
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isDisabled={isSubmitDisabled}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
|
||||||
|
onClick={handleSubmit(onSaveSecret)}
|
||||||
|
className="h-10 text-black"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
>
|
||||||
|
{isRollbackMode ? "Rollback" : "Save Changes"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
isEmptyPage ? "flex flex-col items-center justify-center" : ""
|
||||||
|
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||||
|
ref={secretContainer}
|
||||||
|
>
|
||||||
|
{!isEmptyPage && (
|
||||||
|
<DndContext
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
>
|
||||||
|
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||||
|
<table className="secret-table relative">
|
||||||
|
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||||
|
<tbody className="max-h-96 overflow-y-auto">
|
||||||
|
<SecretImportSection
|
||||||
|
onSecretImportDelete={handleSecretImportDelPopUpOpen}
|
||||||
|
secrets={secrets?.secrets}
|
||||||
|
importedSecrets={importedSecrets}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
<FolderSection
|
||||||
|
onFolderOpen={handleFolderOpen}
|
||||||
|
onFolderUpdate={handleFolderCreatePopUpOpen}
|
||||||
|
onFolderDelete={handleFolderDeletePopUpOpen}
|
||||||
|
folders={folderList}
|
||||||
|
search={searchFilter}
|
||||||
|
/>
|
||||||
|
{fields.map(({ id, _id }, index) => (
|
||||||
|
<SecretInputRow
|
||||||
|
key={id}
|
||||||
|
secUniqId={_id}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
isRollbackMode={isRollbackMode}
|
||||||
|
isAddOnly={isAddOnly}
|
||||||
|
index={index}
|
||||||
|
searchTerm={searchFilter}
|
||||||
|
onSecretDelete={onSecretDelete}
|
||||||
|
isKeyError={Boolean(errors?.secrets?.[index]?.key?.message)}
|
||||||
|
keyError={errors?.secrets?.[index]?.key?.message}
|
||||||
|
onRowExpand={onDrawerOpen}
|
||||||
|
isSecretValueHidden={isSecretValueHidden}
|
||||||
|
wsTags={wsTags}
|
||||||
|
onCreateTagOpen={handleCreateTagModalOpen}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!isReadOnly && !isRollbackMode && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||||
|
onClick={onAppendSecret}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
<span className="ml-2 w-20">Add Secret</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
</DndContext>
|
||||||
|
)}
|
||||||
|
<FormProvider {...method}>
|
||||||
<PitDrawer
|
<PitDrawer
|
||||||
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("secretSnapshots", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("secretSnapshots", isOpen)}
|
||||||
@@ -966,120 +1004,121 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index}
|
index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index}
|
||||||
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
|
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
|
||||||
/>
|
/>
|
||||||
<SecretDropzone
|
</FormProvider>
|
||||||
isSmaller={!isEmptyPage}
|
|
||||||
onParsedEnv={handleUploadedEnv}
|
<SecretDropzone
|
||||||
onAddNewSecret={onAppendSecret}
|
isSmaller={!isEmptyPage}
|
||||||
/>
|
onParsedEnv={handleUploadedEnv}
|
||||||
</div>
|
onAddNewSecret={onAppendSecret}
|
||||||
{/* secrets table and drawers, modals */}
|
/>
|
||||||
</form>
|
</div>
|
||||||
{/* Create a new tag modal */}
|
{/* secrets table and drawers, modals */}
|
||||||
<Modal
|
</form>
|
||||||
isOpen={popUp?.addTag?.isOpen}
|
{/* Create a new tag modal */}
|
||||||
onOpenChange={(open) => {
|
<Modal
|
||||||
handlePopUpToggle("addTag", open);
|
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."
|
||||||
>
|
>
|
||||||
<ModalContent
|
<CreateTagModal onCreateTag={onCreateWsTag} />
|
||||||
title="Create tag"
|
</ModalContent>
|
||||||
subTitle="Specify your tag name, and the slug will be created automatically."
|
</Modal>
|
||||||
>
|
{/* Uploaded env override or not confirmation modal */}
|
||||||
<CreateTagModal onCreateTag={onCreateWsTag} />
|
<Modal
|
||||||
</ModalContent>
|
isOpen={popUp?.uploadedSecOpts?.isOpen}
|
||||||
</Modal>
|
onOpenChange={(open) => handlePopUpToggle("uploadedSecOpts", open)}
|
||||||
{/* Uploaded env override or not confirmation modal */}
|
>
|
||||||
<Modal
|
<ModalContent
|
||||||
isOpen={popUp?.uploadedSecOpts?.isOpen}
|
title="Duplicate Secrets"
|
||||||
onOpenChange={(open) => handlePopUpToggle("uploadedSecOpts", open)}
|
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>
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<div className="flex flex-col space-y-2 text-gray-300">
|
||||||
title="Duplicate Secrets"
|
<div>Your file contains following duplicate secrets</div>
|
||||||
footerContent={[
|
<div className="text-sm text-gray-400">
|
||||||
<Button
|
{Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {})
|
||||||
key="keep-old-btn"
|
?.map((key) => key)
|
||||||
className="mr-4"
|
.join(", ")}
|
||||||
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>
|
</div>
|
||||||
</ModalContent>
|
<div>Are you sure you want to overwrite these secrets?</div>
|
||||||
</Modal>
|
</div>
|
||||||
<Modal
|
</ModalContent>
|
||||||
isOpen={popUp?.folderForm?.isOpen}
|
</Modal>
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("folderForm", isOpen)}
|
<Modal
|
||||||
|
isOpen={popUp?.folderForm?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("folderForm", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title={isEditFolder ? "Edit Folder" : "Create Folder"}>
|
||||||
|
<FolderForm
|
||||||
|
isEdit={isEditFolder}
|
||||||
|
onUpdateFolder={handleFolderUpdate}
|
||||||
|
onCreateFolder={handleFolderCreate}
|
||||||
|
defaultFolderName={(popUp?.folderForm?.data as TEditFolderForm)?.name}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addSecretImport?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Add Secret Link"
|
||||||
|
subTitle="To inherit secrets from another environment or folder"
|
||||||
>
|
>
|
||||||
<ModalContent title={isEditFolder ? "Edit Folder" : "Create Folder"}>
|
<SecretImportForm
|
||||||
<FolderForm
|
environments={currentWorkspace?.environments}
|
||||||
isEdit={isEditFolder}
|
onCreate={handleSecretImportCreate}
|
||||||
onUpdateFolder={handleFolderUpdate}
|
/>
|
||||||
onCreateFolder={handleFolderCreate}
|
</ModalContent>
|
||||||
defaultFolderName={(popUp?.folderForm?.data as TEditFolderForm)?.name}
|
</Modal>
|
||||||
/>
|
<DeleteActionModal
|
||||||
</ModalContent>
|
isOpen={popUp.deleteFolder.isOpen}
|
||||||
</Modal>
|
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
||||||
<Modal
|
title="Do you want to delete this folder?"
|
||||||
isOpen={popUp?.addSecretImport?.isOpen}
|
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
|
onDeleteApproved={handleFolderDelete}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteSecretImport.isOpen}
|
||||||
|
deleteKey="unlink"
|
||||||
|
title="Do you want to remove this secret import?"
|
||||||
|
subTitle={`This will unlink secrets from environment ${
|
||||||
|
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
||||||
|
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||||
|
onDeleteApproved={handleSecretImportDelete}
|
||||||
|
/>
|
||||||
|
<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]"
|
||||||
>
|
>
|
||||||
<ModalContent
|
<CompareSecret
|
||||||
title="Add Secret Link"
|
workspaceId={workspaceId}
|
||||||
subTitle="To inherit secrets from another environment or folder"
|
envs={userAvailableEnvs || []}
|
||||||
>
|
secretKey={popUp?.compareSecrets?.data as string}
|
||||||
<SecretImportForm
|
/>
|
||||||
environments={currentWorkspace?.environments}
|
</ModalContent>
|
||||||
onCreate={handleSecretImportCreate}
|
</Modal>
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.deleteFolder.isOpen}
|
|
||||||
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
|
||||||
title="Do you want to delete this folder?"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
|
||||||
onDeleteApproved={handleFolderDelete}
|
|
||||||
/>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.deleteSecretImport.isOpen}
|
|
||||||
deleteKey="unlink"
|
|
||||||
title="Do you want to remove this secret import?"
|
|
||||||
subTitle={`This will unlink secrets from environment ${
|
|
||||||
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
|
||||||
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
|
||||||
onDeleteApproved={handleSecretImportDelete}
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
{subscription && (
|
{subscription && (
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
|
|||||||
@@ -75,12 +75,29 @@ export type FormData = yup.InferType<typeof schema>;
|
|||||||
export type TSecretDetailsOpen = { index: number; id: string };
|
export type TSecretDetailsOpen = { index: number; id: string };
|
||||||
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
|
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
|
||||||
|
|
||||||
export const downloadSecret = (secrets: FormData["secrets"] = [], env: string = "unknown") => {
|
export const downloadSecret = (
|
||||||
const finalSecret = secrets.map(({ key, value, valueOverride, overrideAction, comment }) => ({
|
secrets: FormData["secrets"] = [],
|
||||||
key,
|
importedSecrets: { key: string; value?: string; comment?: string }[] = [],
|
||||||
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
|
env: string = "unknown"
|
||||||
comment
|
) => {
|
||||||
}));
|
const importSecPos: Record<string, number> = {};
|
||||||
|
importedSecrets.forEach((el, index) => {
|
||||||
|
importSecPos[el.key] = index;
|
||||||
|
});
|
||||||
|
const finalSecret = [...importedSecrets];
|
||||||
|
secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => {
|
||||||
|
const newValue = {
|
||||||
|
key,
|
||||||
|
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
|
||||||
|
comment
|
||||||
|
};
|
||||||
|
// can also be zero thus failing
|
||||||
|
if (typeof importSecPos?.[key] === "undefined") {
|
||||||
|
finalSecret.push(newValue);
|
||||||
|
} else {
|
||||||
|
finalSecret[importSecPos[key]] = newValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let file = "";
|
let file = "";
|
||||||
finalSecret.forEach(({ key, value, comment }) => {
|
finalSecret.forEach(({ key, value, comment }) => {
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
|
||||||
import { useCallback, useRef, useState } from "react";
|
|
||||||
import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
import { useSyntaxHighlight } from "@app/hooks";
|
|
||||||
import { useToggle } from "@app/hooks/useToggle";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
secrets: any[] | undefined;
|
|
||||||
// permission and external state's that decided to hide or show
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
isSecretValueHidden: boolean;
|
|
||||||
userAvailableEnvs?: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const SEC_VAL_LINE_HEIGHT = 21;
|
|
||||||
const MAX_MULTI_LINE = 6;
|
|
||||||
|
|
||||||
const DashboardInput = ({
|
|
||||||
isOverridden,
|
|
||||||
isSecretValueHidden,
|
|
||||||
secret,
|
|
||||||
isReadOnly = true
|
|
||||||
}: {
|
|
||||||
isOverridden: boolean;
|
|
||||||
isSecretValueHidden: boolean;
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
secret?: any;
|
|
||||||
}): JSX.Element => {
|
|
||||||
const ref = useRef<HTMLElement | null>(null);
|
|
||||||
const [isFocused, setIsFocused] = useToggle();
|
|
||||||
const syntaxHighlight = useSyntaxHighlight();
|
|
||||||
|
|
||||||
const value = isOverridden ? secret.valueOverride : secret?.value;
|
|
||||||
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
|
||||||
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<td
|
|
||||||
key={`row-${secret?.key || ""}--`}
|
|
||||||
className={`flex w-full cursor-default flex-row ${
|
|
||||||
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
|
||||||
<textarea
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
value={value}
|
|
||||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
|
||||||
style={{ height: `${maxMultilineHeight}px` }}
|
|
||||||
spellCheck="false"
|
|
||||||
onBlur={() => setIsFocused.off()}
|
|
||||||
onFocus={() => setIsFocused.on()}
|
|
||||||
onInput={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onScroll={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
|
||||||
<code
|
|
||||||
ref={ref}
|
|
||||||
className={`absolute top-1.5 left-3.5 z-10 overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
|
||||||
isOverridden && "text-primary-300"
|
|
||||||
} ${
|
|
||||||
(value || "") === "" && "text-mineshaft-400"
|
|
||||||
}`}
|
|
||||||
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
|
||||||
>
|
|
||||||
{value === undefined ? (
|
|
||||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
|
||||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)
|
|
||||||
)}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EnvComparisonRow = ({
|
|
||||||
secrets,
|
|
||||||
isSecretValueHidden,
|
|
||||||
isReadOnly,
|
|
||||||
userAvailableEnvs
|
|
||||||
}: Props): JSX.Element => {
|
|
||||||
const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true);
|
|
||||||
|
|
||||||
const getSecretByEnv = useCallback(
|
|
||||||
(secEnv: string, secs?: any[]) => secs?.find(({ env }) => env === secEnv),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className="group flex min-w-full flex-row hover:bg-mineshaft-800">
|
|
||||||
<td className="flex w-10 justify-center border-none px-4">
|
|
||||||
<div className="flex h-8 w-10 items-center justify-center text-center text-xs text-bunker-400">
|
|
||||||
<FontAwesomeIcon icon={faKey} />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="flex min-w-[200px] flex-row justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
|
||||||
<div className="flex h-8 cursor-default flex-row items-center justify-center truncate">
|
|
||||||
{secrets![0].key || ""}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="invisible mr-1 ml-2 text-bunker-400 hover:text-bunker-300 group-hover:visible"
|
|
||||||
onClick={() => setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={areValuesHiddenThisRow ? faEye : faEyeSlash} />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
{userAvailableEnvs?.map(({ slug }) => (
|
|
||||||
<DashboardInput
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
key={`row-${secrets![0].key || ""}-${slug}`}
|
|
||||||
isOverridden={false}
|
|
||||||
secret={getSecretByEnv(slug, secrets)}
|
|
||||||
isSecretValueHidden={areValuesHiddenThisRow && isSecretValueHidden}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
folderInEnv: Record<string, boolean>;
|
|
||||||
userAvailableEnvs?: Array<{ slug: string; name: string }>;
|
|
||||||
folderName: string;
|
|
||||||
onClick: (folderName: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FolderComparisonRow = ({
|
|
||||||
folderInEnv = {},
|
|
||||||
userAvailableEnvs = [],
|
|
||||||
folderName,
|
|
||||||
onClick
|
|
||||||
}: Props) => (
|
|
||||||
<tr
|
|
||||||
className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800"
|
|
||||||
onClick={() => onClick(folderName)}
|
|
||||||
>
|
|
||||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
|
||||||
<div className="w-10 text-center text-xs text-bunker-400">
|
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[200px] xl:min-w-[250px]">
|
|
||||||
<div className="flex h-8 flex-row items-center truncate">{folderName}</div>
|
|
||||||
</td>
|
|
||||||
{userAvailableEnvs?.map(({ slug }) => (
|
|
||||||
<td
|
|
||||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
|
||||||
folderInEnv[slug]
|
|
||||||
? "bg-mineshaft-900/30 text-green-500/80"
|
|
||||||
: "bg-red-800/10 text-red-500/80"
|
|
||||||
}`}
|
|
||||||
key={`${folderName}-${slug}`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={folderInEnv[slug] ? faCheck : faXmark} />
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { EnvComparisonRow } from "./EnvComparisonRow";
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
@@ -11,70 +12,74 @@ type Props = {
|
|||||||
onFolderOpen: (folderId: string) => void;
|
onFolderOpen: (folderId: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FolderSection = ({
|
export const FolderSection = memo(
|
||||||
onFolderUpdate: handleFolderUpdate,
|
({
|
||||||
onFolderDelete: handleFolderDelete,
|
onFolderUpdate: handleFolderUpdate,
|
||||||
onFolderOpen: handleFolderOpen,
|
onFolderDelete: handleFolderDelete,
|
||||||
search = "",
|
onFolderOpen: handleFolderOpen,
|
||||||
folders = []
|
search = "",
|
||||||
}: Props) => {
|
folders = []
|
||||||
return (
|
}: Props) => {
|
||||||
<>
|
return (
|
||||||
{folders
|
<>
|
||||||
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
{folders
|
||||||
.map(({ id, name }) => (
|
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
||||||
<tr
|
.map(({ id, name }) => (
|
||||||
key={id}
|
<tr
|
||||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
key={id}
|
||||||
>
|
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||||
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
|
||||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
colSpan={2}
|
|
||||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
|
||||||
style={{ paddingTop: "0", paddingBottom: "0" }}
|
|
||||||
>
|
>
|
||||||
<div
|
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
||||||
className="flex-grow cursor-default p-2"
|
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||||
onKeyDown={() => null}
|
</td>
|
||||||
tabIndex={0}
|
<td
|
||||||
role="button"
|
colSpan={2}
|
||||||
onClick={() => handleFolderOpen(id)}
|
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
||||||
|
style={{ paddingTop: "0", paddingBottom: "0" }}
|
||||||
>
|
>
|
||||||
{name}
|
<div
|
||||||
</div>
|
className="flex-grow cursor-default p-2"
|
||||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
onKeyDown={() => null}
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
tabIndex={0}
|
||||||
<Tooltip content="Settings" className="capitalize">
|
role="button"
|
||||||
<IconButton
|
onClick={() => handleFolderOpen(id)}
|
||||||
size="md"
|
>
|
||||||
colorSchema="primary"
|
{name}
|
||||||
variant="plain"
|
|
||||||
onClick={() => handleFolderUpdate(id, name)}
|
|
||||||
ariaLabel="expand"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||||
<Tooltip content="Delete" className="capitalize">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
<IconButton
|
<Tooltip content="Settings" className="capitalize">
|
||||||
size="md"
|
<IconButton
|
||||||
variant="plain"
|
size="md"
|
||||||
colorSchema="danger"
|
colorSchema="primary"
|
||||||
ariaLabel="delete"
|
variant="plain"
|
||||||
onClick={() => handleFolderDelete(id, name)}
|
onClick={() => handleFolderUpdate(id, name)}
|
||||||
>
|
ariaLabel="expand"
|
||||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
>
|
||||||
</IconButton>
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Delete" className="capitalize">
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="delete"
|
||||||
|
onClick={() => handleFolderDelete(id, name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
))}
|
||||||
))}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
|
FolderSection.displayName = "FolderSection";
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks/useToggle";
|
import { useToggle } from "@app/hooks/useToggle";
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ export const SecretImportItem = ({
|
|||||||
{key}
|
{key}
|
||||||
</td>
|
</td>
|
||||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||||
{value}
|
<SecretInput value={value} isDisabled isVisible />
|
||||||
</td>
|
</td>
|
||||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||||
|
|||||||
+28
-28
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo } from "react";
|
||||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||||
|
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
@@ -62,32 +63,31 @@ type Props = {
|
|||||||
items: { id: string; environment: string; secretPath: string }[];
|
items: { id: string; environment: string; secretPath: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretImportSection = ({
|
export const SecretImportSection = memo(
|
||||||
secrets = [],
|
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [] }: Props) => {
|
||||||
importedSecrets = [],
|
const { currentWorkspace } = useWorkspace();
|
||||||
onSecretImportDelete,
|
const environments = currentWorkspace?.environments || [];
|
||||||
items = []
|
|
||||||
}: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const environments = currentWorkspace?.environments || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||||
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
||||||
<SecretImportItem
|
<SecretImportItem
|
||||||
key={id}
|
key={id}
|
||||||
importedEnv={importSecEnv}
|
importedEnv={importSecEnv}
|
||||||
importedSecrets={computeImportedSecretRows(
|
importedSecrets={computeImportedSecretRows(
|
||||||
importSecEnv,
|
importSecEnv,
|
||||||
impSecPath,
|
impSecPath,
|
||||||
importedSecrets,
|
importedSecrets,
|
||||||
secrets,
|
secrets,
|
||||||
environments
|
environments
|
||||||
)}
|
)}
|
||||||
onDelete={onSecretImportDelete}
|
onDelete={onSecretImportDelete}
|
||||||
importedSecPath={impSecPath}
|
importedSecPath={impSecPath}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
SecretImportSection.displayName = "SecretImportSection";
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useSyntaxHighlight, useToggle } from "@app/hooks";
|
|
||||||
|
|
||||||
import { FormData } from "../../DashboardPage.utils";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
isReadOnly?: boolean;
|
|
||||||
isSecretValueHidden?: boolean;
|
|
||||||
isOverridden?: boolean;
|
|
||||||
index: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SEC_VAL_LINE_HEIGHT = 21;
|
|
||||||
const MAX_MULTI_LINE = 6;
|
|
||||||
|
|
||||||
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
|
||||||
const { control } = useFormContext<FormData>();
|
|
||||||
const ref = useRef<HTMLElement | null>(null);
|
|
||||||
const [isFocused, setIsFocused] = useToggle();
|
|
||||||
const syntaxHighlight = useSyntaxHighlight();
|
|
||||||
|
|
||||||
const secretValue = useWatch({ control, name: `secrets.${index}.value` });
|
|
||||||
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
|
||||||
const value = isOverridden ? secretValueOverride : secretValue;
|
|
||||||
|
|
||||||
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
|
||||||
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
|
||||||
{isOverridden ? (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`secrets.${index}.valueOverride`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<textarea
|
|
||||||
key={`secrets.${index}.valueOverride`}
|
|
||||||
{...field}
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
|
||||||
style={{ height: `${maxMultilineHeight}px` }}
|
|
||||||
spellCheck="false"
|
|
||||||
onBlur={() => setIsFocused.off()}
|
|
||||||
onFocus={() => setIsFocused.on()}
|
|
||||||
onInput={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onScroll={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`secrets.${index}.value`}
|
|
||||||
key={`secrets.${index}.value`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<textarea
|
|
||||||
{...field}
|
|
||||||
readOnly={isReadOnly}
|
|
||||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
|
||||||
style={{ height: `${maxMultilineHeight}px` }}
|
|
||||||
spellCheck="false"
|
|
||||||
onBlur={() => setIsFocused.off()}
|
|
||||||
onFocus={() => setIsFocused.on()}
|
|
||||||
onInput={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onScroll={(el) => {
|
|
||||||
if (ref.current) {
|
|
||||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
|
||||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
|
||||||
<code
|
|
||||||
ref={ref}
|
|
||||||
className={`absolute top-1.5 left-3.5 z-10 w-full overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
|
||||||
isOverridden && "text-primary-300"
|
|
||||||
} ${
|
|
||||||
(value || "") === "" && "text-mineshaft-400"
|
|
||||||
}`}
|
|
||||||
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
|
||||||
>
|
|
||||||
{syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import { memo, useEffect, useRef } from "react";
|
import { memo, useEffect, useRef, useState } from "react";
|
||||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
import {
|
||||||
|
Control,
|
||||||
|
Controller,
|
||||||
|
useFieldArray,
|
||||||
|
UseFormRegister,
|
||||||
|
UseFormSetValue,
|
||||||
|
useWatch
|
||||||
|
} from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
@@ -28,6 +35,7 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
|
SecretInput,
|
||||||
Tag,
|
Tag,
|
||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
@@ -36,24 +44,6 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { WsTag } from "@app/hooks/api/types";
|
import { WsTag } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
||||||
import { MaskedInput } from "./MaskedInput";
|
|
||||||
|
|
||||||
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 = [
|
const tagColors = [
|
||||||
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
||||||
@@ -66,6 +56,31 @@ const tagColors = [
|
|||||||
{ bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" }
|
{ bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
index: number;
|
||||||
|
// backend generated unique id
|
||||||
|
secUniqId?: string;
|
||||||
|
// 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: (secId: string | undefined, index: number) => void;
|
||||||
|
// tag props
|
||||||
|
wsTags?: WsTag[];
|
||||||
|
onCreateTagOpen: () => void;
|
||||||
|
// rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering
|
||||||
|
control: Control<FormData>;
|
||||||
|
register: UseFormRegister<FormData>;
|
||||||
|
setValue: UseFormSetValue<FormData>;
|
||||||
|
isKeyError?: boolean;
|
||||||
|
keyError?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const SecretInputRow = memo(
|
export const SecretInputRow = memo(
|
||||||
({
|
({
|
||||||
index,
|
index,
|
||||||
@@ -77,10 +92,15 @@ export const SecretInputRow = memo(
|
|||||||
wsTags,
|
wsTags,
|
||||||
onCreateTagOpen,
|
onCreateTagOpen,
|
||||||
onSecretDelete,
|
onSecretDelete,
|
||||||
searchTerm
|
searchTerm,
|
||||||
|
control,
|
||||||
|
register,
|
||||||
|
setValue,
|
||||||
|
isKeyError,
|
||||||
|
keyError,
|
||||||
|
secUniqId
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const isKeySubDisabled = useRef<boolean>(false);
|
const isKeySubDisabled = useRef<boolean>(false);
|
||||||
const { register, setValue, control } = useFormContext<FormData>();
|
|
||||||
// comment management in a row
|
// comment management in a row
|
||||||
const {
|
const {
|
||||||
fields: secretTags,
|
fields: secretTags,
|
||||||
@@ -89,28 +109,40 @@ export const SecretInputRow = memo(
|
|||||||
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||||
|
|
||||||
// to get details on a secret
|
// to get details on a secret
|
||||||
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
|
const overrideAction = useWatch({
|
||||||
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` });
|
control,
|
||||||
const secComment = useWatch({ control, name: `secrets.${index}.comment` });
|
name: `secrets.${index}.overrideAction`,
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
|
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride`, exact: true });
|
||||||
|
const secComment = useWatch({ control, name: `secrets.${index}.comment`, exact: true });
|
||||||
const hasComment = Boolean(secComment);
|
const hasComment = Boolean(secComment);
|
||||||
const secKey = useWatch({
|
const secKey = useWatch({
|
||||||
control,
|
control,
|
||||||
name: `secrets.${index}.key`,
|
name: `secrets.${index}.key`,
|
||||||
disabled: isKeySubDisabled.current
|
disabled: isKeySubDisabled.current,
|
||||||
|
exact: true
|
||||||
});
|
});
|
||||||
const secValue = useWatch({
|
const secValue = useWatch({
|
||||||
control,
|
control,
|
||||||
name: `secrets.${index}.value`,
|
name: `secrets.${index}.value`,
|
||||||
disabled: isKeySubDisabled.current
|
disabled: isKeySubDisabled.current,
|
||||||
|
exact: true
|
||||||
});
|
});
|
||||||
const secValueOverride = useWatch({
|
const secValueOverride = useWatch({
|
||||||
control,
|
control,
|
||||||
name: `secrets.${index}.valueOverride`,
|
name: `secrets.${index}.valueOverride`,
|
||||||
disabled: isKeySubDisabled.current
|
disabled: isKeySubDisabled.current,
|
||||||
})
|
exact: true
|
||||||
const secId = useWatch({ control, name: `secrets.${index}._id` });
|
});
|
||||||
|
// when secret is override by personal values
|
||||||
|
const isOverridden =
|
||||||
|
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||||
|
const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue);
|
||||||
|
|
||||||
const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || [];
|
const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true });
|
||||||
|
const tags =
|
||||||
|
useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || [];
|
||||||
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
||||||
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
||||||
{}
|
{}
|
||||||
@@ -126,15 +158,15 @@ export const SecretInputRow = memo(
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isInviteLinkCopied]);
|
}, [isInviteLinkCopied]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEditorRef(isOverridden ? secValueOverride : secValue);
|
||||||
|
}, [isOverridden]);
|
||||||
|
|
||||||
const copyTokenToClipboard = () => {
|
const copyTokenToClipboard = () => {
|
||||||
navigator.clipboard.writeText((secValueOverride || secValue) as string);
|
navigator.clipboard.writeText((secValueOverride || secValue) as string);
|
||||||
setInviteLinkCopied.on();
|
setInviteLinkCopied.on();
|
||||||
};
|
};
|
||||||
|
|
||||||
// when secret is override by personal values
|
|
||||||
const isOverridden =
|
|
||||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
|
||||||
|
|
||||||
const onSecretOverride = () => {
|
const onSecretOverride = () => {
|
||||||
if (isOverridden) {
|
if (isOverridden) {
|
||||||
// when user created a new override but then removes
|
// when user created a new override but then removes
|
||||||
@@ -193,10 +225,10 @@ export const SecretInputRow = memo(
|
|||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
name={`secrets.${index}.key`}
|
name={`secrets.${index}.key`}
|
||||||
render={({ fieldState: { error }, field }) => (
|
render={({ field }) => (
|
||||||
<HoverCard openDelay={0} open={error?.message ? undefined : false}>
|
<HoverCard openDelay={0} open={isKeyError ? undefined : false}>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<td className={cx(error?.message ? "rounded ring ring-red/50" : null)}>
|
<td className={cx(isKeyError ? "rounded ring ring-red/50" : null)}>
|
||||||
<div className="relative flex w-full min-w-[220px] items-center justify-end lg:min-w-[240px] xl:min-w-[280px]">
|
<div className="relative flex w-full min-w-[220px] items-center justify-end lg:min-w-[240px] xl:min-w-[280px]">
|
||||||
<Input
|
<Input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@@ -220,21 +252,70 @@ export const SecretInputRow = memo(
|
|||||||
<div>
|
<div>
|
||||||
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">{error?.message}</div>
|
<div className="text-sm">{keyError}</div>
|
||||||
</div>
|
</div>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
</HoverCard>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<td className="flex w-full flex-grow flex-row border-r border-none border-red">
|
<td
|
||||||
<MaskedInput
|
className="flex w-full flex-grow flex-row border-r border-none border-red"
|
||||||
isReadOnly={
|
style={{ padding: "0.5rem 0 0.5rem 1rem" }}
|
||||||
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
>
|
||||||
}
|
<div className="w-full">
|
||||||
isOverridden={isOverridden}
|
{isOverridden ? (
|
||||||
isSecretValueHidden={isSecretValueHidden}
|
<Controller
|
||||||
index={index}
|
control={control}
|
||||||
/>
|
name={`secrets.${index}.valueOverride`}
|
||||||
|
render={({ field: { onChange, onBlur } }) => (
|
||||||
|
<SecretInput
|
||||||
|
key={`secrets.${index}.valueOverride`}
|
||||||
|
isDisabled={
|
||||||
|
isReadOnly ||
|
||||||
|
isRollbackMode ||
|
||||||
|
(isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||||
|
}
|
||||||
|
value={editorRef}
|
||||||
|
isVisible={!isSecretValueHidden}
|
||||||
|
onChange={(val, html) => {
|
||||||
|
console.log(val);
|
||||||
|
onChange(val);
|
||||||
|
setEditorRef(html);
|
||||||
|
}}
|
||||||
|
onBlur={(html) => {
|
||||||
|
setEditorRef(html);
|
||||||
|
onBlur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`secrets.${index}.value`}
|
||||||
|
render={({ field: { onBlur, onChange } }) => (
|
||||||
|
<SecretInput
|
||||||
|
key={`secrets.${index}.value`}
|
||||||
|
isVisible={!isSecretValueHidden}
|
||||||
|
isDisabled={
|
||||||
|
isReadOnly ||
|
||||||
|
isRollbackMode ||
|
||||||
|
(isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||||
|
}
|
||||||
|
onChange={(val, html) => {
|
||||||
|
onChange(val);
|
||||||
|
setEditorRef(html);
|
||||||
|
}}
|
||||||
|
value={editorRef}
|
||||||
|
onBlur={(html) => {
|
||||||
|
setEditorRef(html);
|
||||||
|
onBlur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="min-w-sm flex">
|
<td className="min-w-sm flex">
|
||||||
<div className="flex h-8 items-center pl-2">
|
<div className="flex h-8 items-center pl-2">
|
||||||
@@ -251,7 +332,7 @@ export const SecretInputRow = memo(
|
|||||||
{slug}
|
{slug}
|
||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
<div className="w-0 group-hover:w-6 overflow-hidden">
|
<div className="w-0 overflow-hidden group-hover:w-6">
|
||||||
<Tooltip content="Copy value">
|
<Tooltip content="Copy value">
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -396,7 +477,7 @@ export const SecretInputRow = memo(
|
|||||||
size="lg"
|
size="lg"
|
||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
onClick={onRowExpand}
|
onClick={() => onRowExpand(secUniqId, index)}
|
||||||
ariaLabel="expand"
|
ariaLabel="expand"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faEllipsis} />
|
<FontAwesomeIcon icon={faEllipsis} />
|
||||||
|
|||||||
@@ -1,19 +1,326 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import { useGetUserWsKey } from "@app/hooks/api";
|
import {
|
||||||
|
useCreateSecretV3,
|
||||||
|
useDeleteSecretV3,
|
||||||
|
useGetFoldersByEnv,
|
||||||
|
useGetProjectSecretsAllEnv,
|
||||||
|
useGetUserWsEnvironments,
|
||||||
|
useGetUserWsKey,
|
||||||
|
useUpdateSecretV3
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
|
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
|
||||||
|
import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow";
|
||||||
|
|
||||||
export const SecretOverviewPage = () => {
|
export const SecretOverviewPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { currentWorkspace, isLoading } = useWorkspace();
|
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const workspaceId = currentWorkspace?._id as string;
|
const workspaceId = currentWorkspace?._id as string;
|
||||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
const secretPath = router.query?.secretPath as string;
|
const secretPath = router.query?.secretPath as string;
|
||||||
return <div>Secret overview page</div>;
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWorkspaceLoading && !workspaceId && router.isReady) {
|
||||||
|
router.push(`/org/${currentOrg?._id}/overview`);
|
||||||
|
}
|
||||||
|
}, [isWorkspaceLoading, workspaceId, router.isReady]);
|
||||||
|
|
||||||
|
const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
const userAvailableEnvs = wsEnv?.filter(({ isReadDenied }) => !isReadDenied) || [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: secrets,
|
||||||
|
getSecretByKey,
|
||||||
|
secKeys,
|
||||||
|
getEnvSecretKeyCount
|
||||||
|
} = useGetProjectSecretsAllEnv({
|
||||||
|
workspaceId,
|
||||||
|
envs: userAvailableEnvs.map(({ slug }) => slug),
|
||||||
|
secretPath,
|
||||||
|
decryptFileKey: latestFileKey!
|
||||||
|
});
|
||||||
|
const { folders, folderNames, isFolderPresentInEnv } = useGetFoldersByEnv({
|
||||||
|
workspaceId,
|
||||||
|
environments: userAvailableEnvs.map(({ slug }) => slug),
|
||||||
|
parentFolderPath: secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||||
|
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||||
|
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||||
|
|
||||||
|
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await createSecretV3({
|
||||||
|
environment: env,
|
||||||
|
workspaceId,
|
||||||
|
secretPath,
|
||||||
|
secretName: key,
|
||||||
|
secretValue: value,
|
||||||
|
secretComment: "",
|
||||||
|
type: "shared",
|
||||||
|
latestFileKey: latestFileKey!
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully created secret"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to create secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecretUpdate = async (env: string, key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await updateSecretV3({
|
||||||
|
environment: env,
|
||||||
|
workspaceId,
|
||||||
|
secretPath,
|
||||||
|
secretName: key,
|
||||||
|
secretValue: value,
|
||||||
|
type: "shared",
|
||||||
|
latestFileKey: latestFileKey!
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully updated secret"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSecretDelete = async (env: string, key: string) => {
|
||||||
|
try {
|
||||||
|
await deleteSecretV3({
|
||||||
|
environment: env,
|
||||||
|
workspaceId,
|
||||||
|
secretPath,
|
||||||
|
secretName: key,
|
||||||
|
type: "shared"
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully deleted secret"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to delete secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetSearch = () => setSearchFilter("");
|
||||||
|
|
||||||
|
const handleFolderClick = (path: string) => {
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
secretPath: `${router.query?.secretPath || ""}/${path}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExploreEnvClick = (slug: string) => {
|
||||||
|
const query: Record<string, string> = { ...router.query, env: slug };
|
||||||
|
delete query.secretPath;
|
||||||
|
// the dir return will have the present directory folder id
|
||||||
|
// use that when clicking on explore to redirect user to there
|
||||||
|
const envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug);
|
||||||
|
if (envIndex !== -1) {
|
||||||
|
const envFolder = folders?.[envIndex];
|
||||||
|
const dir = envFolder?.data?.dir?.pop();
|
||||||
|
if (dir) {
|
||||||
|
query.folderId = dir.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push({
|
||||||
|
pathname: "/project/[id]/secrets/[env]",
|
||||||
|
query
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEnvListLoading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex h-screen 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTableLoading =
|
||||||
|
folders?.some(({ isLoading }) => isLoading) && secrets?.some(({ isLoading }) => isLoading);
|
||||||
|
|
||||||
|
const filteredSecretNames = secKeys?.filter((name) =>
|
||||||
|
name.toUpperCase().includes(searchFilter.toUpperCase())
|
||||||
|
);
|
||||||
|
const filteredFolderNames = folderNames?.filter((name) =>
|
||||||
|
name.toLowerCase().includes(searchFilter.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<div className="relative right-5 ml-4">
|
||||||
|
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<p className="text-3xl font-semibold text-bunker-100">Secrets Overview</p>
|
||||||
|
<p className="text-md text-bunker-300">
|
||||||
|
Inject your secrets using
|
||||||
|
<a
|
||||||
|
className="mx-1 text-primary/80 hover:text-primary"
|
||||||
|
href="https://infisical.com/docs/cli/overview"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Infisical CLI
|
||||||
|
</a>
|
||||||
|
or
|
||||||
|
<a
|
||||||
|
className="mx-1 text-primary/80 hover:text-primary"
|
||||||
|
href="https://infisical.com/docs/sdks/overview"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Infisical SDKs
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center justify-between">
|
||||||
|
<FolderBreadCrumbs secretPath={secretPath} onResetSearch={handleResetSearch} />
|
||||||
|
<div className="w-80">
|
||||||
|
<Input
|
||||||
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
|
placeholder="Search by secret/folder name..."
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="sticky left-0 z-10 bg-clip-padding">Secret</Th>
|
||||||
|
{userAvailableEnvs?.map(({ name, slug }, index) => {
|
||||||
|
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
||||||
|
const missingKeyCount = secKeys.length - envSecKeyCount;
|
||||||
|
return (
|
||||||
|
<Th
|
||||||
|
className="min-table-row min-w-[11rem] text-center"
|
||||||
|
key={`secret-overview-${name}-${index + 1}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
{name}
|
||||||
|
{missingKeyCount > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-none lowercase"
|
||||||
|
content={`${missingKeyCount} secrets missing\n compared to other environments`}
|
||||||
|
>
|
||||||
|
<div className="ml-2 flex h-5 w-5 cursor-default items-center justify-center rounded-sm bg-red-700 text-xs text-bunker-100">
|
||||||
|
<span className="text-bunker-100">{missingKeyCount}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isTableLoading && (
|
||||||
|
<TableSkeleton
|
||||||
|
columns={userAvailableEnvs.length + 1}
|
||||||
|
innerKey="secret-overview-loading"
|
||||||
|
rows={5}
|
||||||
|
className="bg-mineshaft-700"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filteredFolderNames.map((folderName, index) => (
|
||||||
|
<SecretOverviewFolderRow
|
||||||
|
folderName={folderName}
|
||||||
|
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||||
|
environments={userAvailableEnvs}
|
||||||
|
key={`overview-${folderName}-${index + 1}`}
|
||||||
|
onClick={handleFolderClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{filteredSecretNames.map((key, index) => (
|
||||||
|
<SecretOverviewTableRow
|
||||||
|
onSecretCreate={handleSecretCreate}
|
||||||
|
onSecretDelete={handleSecretDelete}
|
||||||
|
onSecretUpdate={handleSecretUpdate}
|
||||||
|
key={`overview-${key}-${index + 1}`}
|
||||||
|
environments={userAvailableEnvs}
|
||||||
|
secretKey={key}
|
||||||
|
getSecretByKey={getSecretByKey}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Tr>
|
||||||
|
<Td />
|
||||||
|
{userAvailableEnvs.map(({ name, slug }) => (
|
||||||
|
<Td key={`explore-${name}-btn`} className=" border-x border-mineshaft-700">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => handleExploreEnvClick(slug)}
|
||||||
|
>
|
||||||
|
Explore
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretPath: string;
|
||||||
|
onResetSearch: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FolderBreadCrumbs = ({ secretPath = "/", onResetSearch }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onFolderCrumbClick = (index: number) => {
|
||||||
|
const newSecPath = secretPath.split("/").filter(Boolean).slice(0, index).join("/");
|
||||||
|
if (secretPath === `/${newSecPath}`) return;
|
||||||
|
const query = { ...router.query, secretPath: `/${newSecPath}` } as Record<string, string>;
|
||||||
|
// root condition
|
||||||
|
if (index === 0) delete query.secretPath;
|
||||||
|
router
|
||||||
|
.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query
|
||||||
|
})
|
||||||
|
.then(() => onResetSearch());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
className="breadcrumb relative z-20 border-solid border-mineshaft-600 bg-mineshaft-800 py-1 pl-5 pr-2 text-sm hover:bg-mineshaft-600"
|
||||||
|
onClick={() => onFolderCrumbClick(0)}
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFolderOpen} className="text-primary" />
|
||||||
|
</div>
|
||||||
|
{(secretPath || "")
|
||||||
|
.split("/")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((path, index, arr) => (
|
||||||
|
<div
|
||||||
|
key={`secret-path-${index + 1}`}
|
||||||
|
className={`breadcrumb relative z-20 ${
|
||||||
|
index + 1 === arr.length ? "cursor-default" : "cursor-pointer"
|
||||||
|
} border-solid border-mineshaft-600 py-1 pl-5 pr-2 text-sm text-mineshaft-200`}
|
||||||
|
onClick={() => onFolderCrumbClick(index + 1)}
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Td, Tr } from "@app/components/v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
folderName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isFolderPresentInEnv: (name: string, env: string) => boolean;
|
||||||
|
onClick: (path: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretOverviewFolderRow = ({
|
||||||
|
folderName,
|
||||||
|
environments = [],
|
||||||
|
isFolderPresentInEnv,
|
||||||
|
onClick
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Tr isHoverable isSelectable className="group" onClick={() => onClick(folderName)}>
|
||||||
|
<Td className="sticky left-0 z-10 border-x border-mineshaft-700 bg-mineshaft-800 bg-clip-padding py-3 group-hover:bg-mineshaft-600">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="text-primary">
|
||||||
|
<FontAwesomeIcon icon={faFolder} />
|
||||||
|
</div>
|
||||||
|
<div>{folderName}</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isFolderPresentInEnv(folderName, slug);
|
||||||
|
return (
|
||||||
|
<Td
|
||||||
|
key={`sec-overview-${slug}-${i + 1}-folder`}
|
||||||
|
className={twMerge(
|
||||||
|
"border-x border-mineshaft-700 py-3",
|
||||||
|
isPresent ? "text-green-600" : "text-red-800"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<FontAwesomeIcon icon={isPresent ? faCheck : faXmark} />
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretOverviewFolderRow } from "./SecretOverviewFolderRow";
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { IconButton, SecretInput, Tooltip } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultValue?: string | null;
|
||||||
|
secretName: string;
|
||||||
|
isCreatable?: boolean;
|
||||||
|
isVisible?: boolean;
|
||||||
|
environment: string;
|
||||||
|
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretDelete: (env: string, key: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretEditRow = ({
|
||||||
|
defaultValue,
|
||||||
|
isCreatable,
|
||||||
|
onSecretUpdate,
|
||||||
|
secretName,
|
||||||
|
onSecretCreate,
|
||||||
|
onSecretDelete,
|
||||||
|
environment,
|
||||||
|
isVisible
|
||||||
|
}: Props) => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
getValues,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = useForm({
|
||||||
|
values: {
|
||||||
|
value: defaultValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const editorRef = useRef(defaultValue);
|
||||||
|
const [isDeleting, setIsDeleting] = useToggle();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
|
const handleFormReset = () => {
|
||||||
|
reset();
|
||||||
|
const val = getValues();
|
||||||
|
editorRef.current = val.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopySecretToClipboard = async () => {
|
||||||
|
const { value } = getValues();
|
||||||
|
if (value) {
|
||||||
|
try {
|
||||||
|
await window.navigator.clipboard.writeText(value);
|
||||||
|
createNotification({ type: "success", text: "Copied secret to clipboard" });
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
createNotification({ type: "error", text: "Failed to copy secret to clipboard" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async ({ value }: { value?: string | null }) => {
|
||||||
|
if (value && secretName) {
|
||||||
|
if (isCreatable) {
|
||||||
|
await onSecretCreate(environment, secretName, value);
|
||||||
|
} else {
|
||||||
|
await onSecretUpdate(environment, secretName, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reset({ value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSecret = async () => {
|
||||||
|
setIsDeleting.on();
|
||||||
|
try {
|
||||||
|
await onSecretDelete(environment, secretName);
|
||||||
|
reset({ value: undefined });
|
||||||
|
editorRef.current = undefined;
|
||||||
|
} finally {
|
||||||
|
setIsDeleting.off();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex w-full cursor-text items-start space-x-2">
|
||||||
|
<div className="flex-grow border-r border-r-mineshaft-600 pr-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="value"
|
||||||
|
render={({ field: { onChange, onBlur } }) => (
|
||||||
|
<SecretInput
|
||||||
|
value={editorRef.current}
|
||||||
|
onChange={(val, html) => {
|
||||||
|
onChange(val);
|
||||||
|
editorRef.current = html;
|
||||||
|
}}
|
||||||
|
onBlur={(html) => {
|
||||||
|
editorRef.current = html;
|
||||||
|
onBlur();
|
||||||
|
}}
|
||||||
|
isVisible={isVisible}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-16 justify-center space-x-3 pl-2 transition-all">
|
||||||
|
{isDirty ? (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Tooltip content="save">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="submit-value"
|
||||||
|
className="h-full"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onClick={handleSubmit(handleFormSubmit)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Tooltip content="cancel">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="reset-value"
|
||||||
|
className="h-full"
|
||||||
|
onClick={handleFormReset}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Copy Secret">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy-value"
|
||||||
|
onClick={handleCopySecretToClipboard}
|
||||||
|
variant="plain"
|
||||||
|
className="h-full"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Delete">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="delete-value"
|
||||||
|
className="h-full"
|
||||||
|
onClick={handleDeleteSecret}
|
||||||
|
isDisabled={isDeleting}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
import { faCheck, faEye, faEyeSlash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { Button, TableContainer, Td, Tr } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||||
|
|
||||||
|
import { SecretEditRow } from "./SecretEditRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretKey: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
||||||
|
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
|
||||||
|
onSecretDelete: (env: string, key: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretOverviewTableRow = ({
|
||||||
|
secretKey,
|
||||||
|
environments = [],
|
||||||
|
getSecretByKey,
|
||||||
|
onSecretUpdate,
|
||||||
|
onSecretCreate,
|
||||||
|
onSecretDelete
|
||||||
|
}: Props) => {
|
||||||
|
const [isFormExpanded, setIsFormExpanded] = useToggle();
|
||||||
|
const totalCols = environments.length + 1; // secret key row
|
||||||
|
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group">
|
||||||
|
<Td className="sticky left-0 z-10 border-x border-mineshaft-700 bg-mineshaft-800 bg-clip-padding py-3 group-hover:bg-mineshaft-600">
|
||||||
|
{secretKey}
|
||||||
|
</Td>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const secret = getSecretByKey(slug, secretKey);
|
||||||
|
const isSecretPresent = Boolean(secret);
|
||||||
|
return (
|
||||||
|
<Td
|
||||||
|
key={`sec-overview-${slug}-${i + 1}-value`}
|
||||||
|
className={twMerge(
|
||||||
|
"border-x border-mineshaft-700 py-3",
|
||||||
|
isSecretPresent ? "text-green-600" : "text-red-800"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
{isFormExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={totalCols}>
|
||||||
|
<div className="rounded-md bg-bunker-700 p-4 pb-6">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="text-lg font-medium">Secrets</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
className="p-1"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />}
|
||||||
|
onClick={() => setIsSecretVisible.toggle()}
|
||||||
|
>
|
||||||
|
{isSecretVisible ? "Hide" : "Reveal"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TableContainer>
|
||||||
|
<table className="secret-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
style={{ padding: "0.5rem 1rem" }}
|
||||||
|
className="min-table-row min-w-[11rem]"
|
||||||
|
>
|
||||||
|
Environment
|
||||||
|
</th>
|
||||||
|
<th style={{ padding: "0.5rem 1rem" }}>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{environments.map(({ name, slug }) => {
|
||||||
|
const secret = getSecretByKey(slug, secretKey);
|
||||||
|
const isCreatable = !secret;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={`secret-expanded-${slug}-${secretKey}`}
|
||||||
|
className="hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<td className="flex" style={{ padding: "0.25rem 1rem" }}>
|
||||||
|
<div className="flex h-10 items-center">{name}</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="h-10 border-l border-mineshaft-600"
|
||||||
|
style={{ padding: "0.5rem 1rem" }}
|
||||||
|
>
|
||||||
|
<SecretEditRow
|
||||||
|
isVisible={isSecretVisible}
|
||||||
|
secretName={secretKey}
|
||||||
|
defaultValue={secret?.value}
|
||||||
|
isCreatable={isCreatable}
|
||||||
|
onSecretDelete={onSecretDelete}
|
||||||
|
onSecretCreate={onSecretCreate}
|
||||||
|
onSecretUpdate={onSecretUpdate}
|
||||||
|
environment={slug}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SecretOverviewTableRow } from "./SecretOverviewTableRow";
|
||||||
Reference in New Issue
Block a user