feat: implemented new overview page with improvement in dashboard

This commit is contained in:
akhilmhdh
2023-07-27 16:28:50 +05:30
parent bca14dd5c4
commit 7bbbdcc58b
22 changed files with 1729 additions and 1154 deletions

View File

@@ -67,6 +67,7 @@
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.1",
"react-code-input": "^3.10.1",
"react-contenteditable": "^3.3.7",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.4",
"react-hook-form": "^7.43.0",
@@ -75,6 +76,7 @@
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"sanitize-html": "^2.11.0",
"set-cookie-parser": "^2.5.1",
"sharp": "^0.32.0",
"styled-components": "^5.3.7",
@@ -99,6 +101,7 @@
"@types/jsrp": "^0.2.4",
"@types/node": "18.11.9",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.7",
@@ -7922,6 +7925,89 @@
"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": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
@@ -10745,7 +10831,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11203,7 +11288,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true,
"funding": [
{
"type": "github",
@@ -12690,8 +12774,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-diff": {
"version": "1.3.0",
@@ -17860,6 +17943,11 @@
"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": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -18113,7 +18201,6 @@
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -18936,6 +19023,18 @@
"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": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
@@ -20017,6 +20116,88 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"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": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
@@ -28425,6 +28606,66 @@
"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": {
"version": "0.16.3",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
@@ -30633,8 +30874,7 @@
"deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="
},
"default-browser": {
"version": "4.0.0",
@@ -30963,8 +31203,7 @@
"domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"dev": true
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
},
"domhandler": {
"version": "4.3.1",
@@ -32126,8 +32365,7 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-diff": {
"version": "1.3.0",
@@ -35865,6 +36103,11 @@
"dev": 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": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -36081,7 +36324,6 @@
"version": "8.4.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
"integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
"dev": true,
"requires": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
@@ -36653,6 +36895,15 @@
"dev": true,
"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": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
@@ -37421,6 +37672,65 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"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": {
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",

View File

@@ -75,6 +75,7 @@
"react": "^17.0.2",
"react-beautiful-dnd": "^13.1.1",
"react-code-input": "^3.10.1",
"react-contenteditable": "^3.3.7",
"react-dom": "^17.0.2",
"react-grid-layout": "^1.3.4",
"react-hook-form": "^7.43.0",
@@ -83,6 +84,7 @@
"react-markdown": "^8.0.3",
"react-redux": "^8.0.2",
"react-table": "^7.8.0",
"sanitize-html": "^2.11.0",
"set-cookie-parser": "^2.5.1",
"sharp": "^0.32.0",
"styled-components": "^5.3.7",
@@ -107,6 +109,7 @@
"@types/jsrp": "^0.2.4",
"@types/node": "18.11.9",
"@types/react": "^18.0.26",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.7",

View File

@@ -13,6 +13,11 @@
.flex-3 {
flex-grow: 3;
}
.min-table-row {
width: 1%;
white-space: nowrap;
}
}
@layer components {
@@ -40,7 +45,7 @@
.breadcrumb::after,
.breadcrumb::before {
content: '';
content: "";
height: 60%;
width: 100%;
z-index: -1;
@@ -58,18 +63,32 @@
}
.breadcrumb::after {
left: 5px;
bottom: -3px;
left: 4px;
bottom: -2.5px;
transform: skew(-30deg);
}
.breadcrumb::before {
left: 5px;
top: -3px;
left: 4px;
top: -2.5px;
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/500.css';
@import '@fontsource/inter/700.css';
@import "@fontsource/inter/400.css";
@import "@fontsource/inter/500.css";
@import "@fontsource/inter/700.css";

View File

@@ -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>
);
};

View File

@@ -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 { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
@@ -30,7 +30,11 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
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 { 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
* They will get it back
*/
export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
export const DashboardPage = () => {
const { subscription } = useSubscription();
const { t } = useTranslation();
const router = useRouter();
const { createNotification } = useNotificationContext();
const queryClient = useQueryClient();
const envQuery = router.query.env as string;
const secretContainer = useRef<HTMLDivElement | null>(null);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
@@ -172,8 +177,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
onSuccess: (data) => {
// get an env with one of the access available
const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
if (env && data?.map((wsenv) => wsenv.slug).includes(envFromTop)) {
setSelectedEnv(data?.filter((dp) => dp.slug === envFromTop)[0]);
if (env && data?.map((wsenv) => wsenv.slug).includes(envQuery)) {
setSelectedEnv(data?.filter((dp) => dp.slug === envQuery)[0]);
}
}
});
@@ -293,11 +298,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
});
const {
register,
control,
handleSubmit,
getValues,
setValue,
formState: { isSubmitting, isDirty },
formState: { isSubmitting, isDirty, errors },
reset
} = method;
const { fields, prepend, append, remove } = useFieldArray({ control, name: "secrets" });
@@ -461,9 +467,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
}
};
const onDrawerOpen = (dto: TSecretDetailsOpen) => {
handlePopUpOpen("secretDetails", dto);
};
const onDrawerOpen = useCallback((id: string | undefined, index: number) => {
handlePopUpOpen("secretDetails", { id, index } as TSecretDetailsOpen);
}, []);
const onEnvChange = (slug: string) => {
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
// 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 (overrideId) deletedSecretIds.current.push(overrideId);
remove(index);
// just the case if this is called from drawer
handlePopUpClose("secretDetails");
};
}, []);
const onCreateWsTag = async (tagName: string) => {
const onCreateWsTag = useCallback(async (tagName: string) => {
try {
await createWsTag({
workspaceID: workspaceId,
@@ -509,10 +525,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
type: "error"
});
}
};
}, []);
const handleFolderOpen = (id: string) => {
const handleFolderOpen = useCallback((id: string) => {
setSearchFilter("");
console.log(router.query);
router.push({
pathname: router.pathname,
query: {
@@ -520,10 +537,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
folderId: id
}
});
};
}, []);
const isEditFolder = Boolean(popUp?.folderForm?.data);
// FOLDER SECTION
const handleFolderCreate = async (name: string) => {
try {
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;
try {
await updateFolder({
@@ -567,9 +585,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
type: "error"
});
}
};
}, []);
const handleFolderDelete = async () => {
const handleFolderDelete = useCallback(async () => {
const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm;
try {
deleteFolder({
@@ -589,8 +607,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
type: "error"
});
}
};
}, []);
// SECRET IMPORT SECTION
const handleSecretImportCreate = async (env: string, secretPath: string) => {
try {
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
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
@@ -693,259 +731,259 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
return (
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
<FormProvider {...method}>
<form autoComplete="off" className="h-full">
{/* breadcrumb row */}
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={
userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || ""
}
isFolderMode
folders={folderData?.dir}
isProjectRelated
userAvailableEnvs={userAvailableEnvs}
onEnvChange={onEnvChange}
<form autoComplete="off" className="h-full">
{/* breadcrumb row */}
<div className="relative right-6 -top-2 mb-2 ml-6">
<NavHeader
pageName={t("dashboard.title")}
currentEnv={userAvailableEnvs?.filter((envir) => envir.slug === envQuery)[0].name || ""}
isFolderMode
folders={folderData?.dir}
isProjectRelated
userAvailableEnvs={userAvailableEnvs}
onEnvChange={onEnvChange}
/>
</div>
<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 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 className="flex items-center space-x-2">
<div>
<Popover>
<PopoverTrigger asChild>
<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={handleDownloadSecret}
colorSchema="primary"
variant="outline_bg"
className="h-8 bg-bunker-700"
>
Download as .env
</Button>
</div>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center space-x-2">
<div>
<Popover>
<PopoverTrigger asChild>
<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
<div>
<Tooltip content={isSecretValueHidden ? "Reveal Secrets" : "Hide secrets"}>
<IconButton
ariaLabel="reveal"
variant="outline_bg"
onClick={() => {
if (subscription && subscription.pitRecovery) {
handlePopUpOpen("secretSnapshots");
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
isLoading={isLoadingSnapshotCount}
isDisabled={!canDoRollback}
className="h-10"
onClick={() => setIsSecretValueHidden.toggle()}
>
{snapshotCount} Commits
</Button>
</div>
{!isReadOnly && !isRollbackMode && (
<div className="flex flex-row items-center justify-center">
<button
type="button"
onClick={() => {
if (!(isReadOnly || isRollbackMode)) {
if (secretContainer.current) {
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"
<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")}
>
Go back
</Button>
)}
<FontAwesomeIcon icon={faCodeCommit} />
</IconButton>
</Tooltip>
</div>
<div className="hidden xl:block">
<Button
isDisabled={isSubmitDisabled}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={isRollbackMode ? faClockRotateLeft : faCheck} />}
onClick={handleSubmit(onSaveSecret)}
className="h-10 text-black"
color="primary"
variant="solid"
variant="outline_bg"
onClick={() => {
if (subscription && subscription.pitRecovery) {
handlePopUpOpen("secretSnapshots");
return;
}
handlePopUpOpen("upgradePlan");
}}
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
isLoading={isLoadingSnapshotCount}
isDisabled={!canDoRollback}
className="h-10"
>
{isRollbackMode ? "Rollback" : "Save Changes"}
{snapshotCount} Commits
</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={(impSecEnv, impSecPath) =>
handlePopUpOpen("deleteSecretImport", {
environment: impSecEnv,
secretPath: impSecPath
})
}
secrets={secrets?.secrets}
importedSecrets={importedSecrets}
items={items}
/>
<FolderSection
onFolderOpen={handleFolderOpen}
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
folders={folderList}
search={searchFilter}
/>
{fields.map(({ id, _id }, index) => (
<SecretInputRow
key={id}
isReadOnly={isReadOnly}
isRollbackMode={isRollbackMode}
isAddOnly={isAddOnly}
index={index}
searchTerm={searchFilter}
onSecretDelete={onSecretDelete}
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
isSecretValueHidden={isSecretValueHidden}
wsTags={wsTags}
onCreateTagOpen={() => handlePopUpOpen("addTag")}
/>
))}
{!isReadOnly && !isRollbackMode && (
<tr>
<td colSpan={3} className="hover:bg-mineshaft-700">
<button
type="button"
className="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>
{!isReadOnly && !isRollbackMode && (
<div className="flex flex-row items-center justify-center">
<button
type="button"
onClick={() => {
if (!(isReadOnly || isRollbackMode)) {
if (secretContainer.current) {
secretContainer.current.scroll({
top: 0,
behavior: "smooth"
});
}
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
setSearchFilter("");
}
}}
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]"
>
<FontAwesomeIcon icon={faPlus} className="px-2" />
Add Secret
</button>
<DropdownMenu>
<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" />
</div>
</DropdownMenuTrigger>
<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">
<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
</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
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("secretSnapshots", isOpen)}
@@ -966,120 +1004,121 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
index={(popUp?.secretDetails?.data as TSecretDetailsOpen)?.index}
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
/>
<SecretDropzone
isSmaller={!isEmptyPage}
onParsedEnv={handleUploadedEnv}
onAddNewSecret={onAppendSecret}
/>
</div>
{/* secrets table and drawers, modals */}
</form>
{/* Create a new tag modal */}
<Modal
isOpen={popUp?.addTag?.isOpen}
onOpenChange={(open) => {
handlePopUpToggle("addTag", open);
}}
</FormProvider>
<SecretDropzone
isSmaller={!isEmptyPage}
onParsedEnv={handleUploadedEnv}
onAddNewSecret={onAppendSecret}
/>
</div>
{/* secrets table and drawers, modals */}
</form>
{/* Create a new tag modal */}
<Modal
isOpen={popUp?.addTag?.isOpen}
onOpenChange={(open) => {
handlePopUpToggle("addTag", open);
}}
>
<ModalContent
title="Create tag"
subTitle="Specify your tag name, and the slug will be created automatically."
>
<ModalContent
title="Create tag"
subTitle="Specify your tag name, and the slug will be created automatically."
>
<CreateTagModal onCreateTag={onCreateWsTag} />
</ModalContent>
</Modal>
{/* Uploaded env override or not confirmation modal */}
<Modal
isOpen={popUp?.uploadedSecOpts?.isOpen}
onOpenChange={(open) => handlePopUpToggle("uploadedSecOpts", open)}
<CreateTagModal onCreateTag={onCreateWsTag} />
</ModalContent>
</Modal>
{/* Uploaded env override or not confirmation modal */}
<Modal
isOpen={popUp?.uploadedSecOpts?.isOpen}
onOpenChange={(open) => handlePopUpToggle("uploadedSecOpts", open)}
>
<ModalContent
title="Duplicate Secrets"
footerContent={[
<Button
key="keep-old-btn"
className="mr-4"
onClick={() => handlePopUpClose("uploadedSecOpts")}
>
Keep old
</Button>,
<Button colorSchema="danger" key="overwrite-btn" onClick={onOverwriteSecrets}>
Overwrite
</Button>
]}
>
<ModalContent
title="Duplicate Secrets"
footerContent={[
<Button
key="keep-old-btn"
className="mr-4"
onClick={() => handlePopUpClose("uploadedSecOpts")}
>
Keep old
</Button>,
<Button colorSchema="danger" key="overwrite-btn" onClick={onOverwriteSecrets}>
Overwrite
</Button>
]}
>
<div className="flex flex-col space-y-2 text-gray-300">
<div>Your file contains following duplicate secrets</div>
<div className="text-sm text-gray-400">
{Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {})
?.map((key) => key)
.join(", ")}
</div>
<div>Are you sure you want to overwrite these secrets?</div>
<div 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>
</ModalContent>
</Modal>
<Modal
isOpen={popUp?.folderForm?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("folderForm", isOpen)}
<div>Are you sure you want to overwrite these secrets?</div>
</div>
</ModalContent>
</Modal>
<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"}>
<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)}
<SecretImportForm
environments={currentWorkspace?.environments}
onCreate={handleSecretImportCreate}
/>
</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]"
>
<ModalContent
title="Add Secret Link"
subTitle="To inherit secrets from another environment or folder"
>
<SecretImportForm
environments={currentWorkspace?.environments}
onCreate={handleSecretImportCreate}
/>
</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>
<CompareSecret
workspaceId={workspaceId}
envs={userAvailableEnvs || []}
secretKey={popUp?.compareSecrets?.data as string}
/>
</ModalContent>
</Modal>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}

View File

@@ -75,12 +75,29 @@ export type FormData = yup.InferType<typeof schema>;
export type TSecretDetailsOpen = { index: number; id: string };
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
export const downloadSecret = (secrets: FormData["secrets"] = [], env: string = "unknown") => {
const finalSecret = secrets.map(({ key, value, valueOverride, overrideAction, comment }) => ({
key,
value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
comment
}));
export const downloadSecret = (
secrets: FormData["secrets"] = [],
importedSecrets: { key: string; value?: string; comment?: string }[] = [],
env: string = "unknown"
) => {
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 = "";
finalSecret.forEach(({ key, value, comment }) => {

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -1 +0,0 @@
export { EnvComparisonRow } from "./EnvComparisonRow";

View File

@@ -1,3 +1,4 @@
import { memo } from "react";
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -11,70 +12,74 @@ type Props = {
onFolderOpen: (folderId: string) => void;
};
export const FolderSection = ({
onFolderUpdate: handleFolderUpdate,
onFolderDelete: handleFolderDelete,
onFolderOpen: handleFolderOpen,
search = "",
folders = []
}: Props) => {
return (
<>
{folders
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
.map(({ id, name }) => (
<tr
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" }}
export const FolderSection = memo(
({
onFolderUpdate: handleFolderUpdate,
onFolderDelete: handleFolderDelete,
onFolderOpen: handleFolderOpen,
search = "",
folders = []
}: Props) => {
return (
<>
{folders
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
.map(({ id, name }) => (
<tr
key={id}
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
>
<div
className="flex-grow cursor-default p-2"
onKeyDown={() => null}
tabIndex={0}
role="button"
onClick={() => handleFolderOpen(id)}
<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" }}
>
{name}
</div>
<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">
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings" className="capitalize">
<IconButton
size="md"
colorSchema="primary"
variant="plain"
onClick={() => handleFolderUpdate(id, name)}
ariaLabel="expand"
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
<div
className="flex-grow cursor-default p-2"
onKeyDown={() => null}
tabIndex={0}
role="button"
onClick={() => handleFolderOpen(id)}
>
{name}
</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 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">
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings" className="capitalize">
<IconButton
size="md"
colorSchema="primary"
variant="plain"
onClick={() => handleFolderUpdate(id, name)}
ariaLabel="expand"
>
<FontAwesomeIcon icon={faEdit} />
</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>
</td>
</tr>
))}
</>
);
};
</td>
</tr>
))}
</>
);
}
);
FolderSection.displayName = "FolderSection";

View File

@@ -9,7 +9,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
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 { useToggle } from "@app/hooks/useToggle";
@@ -138,7 +138,7 @@ export const SecretImportItem = ({
{key}
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
{value}
<SecretInput value={value} isDisabled isVisible />
</td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />

View File

@@ -1,3 +1,4 @@
import { memo } from "react";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useWorkspace } from "@app/context";
@@ -62,32 +63,31 @@ type Props = {
items: { id: string; environment: string; secretPath: string }[];
};
export const SecretImportSection = ({
secrets = [],
importedSecrets = [],
onSecretImportDelete,
items = []
}: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
export const SecretImportSection = memo(
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [] }: Props) => {
const { currentWorkspace } = useWorkspace();
const environments = currentWorkspace?.environments || [];
return (
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
<SecretImportItem
key={id}
importedEnv={importSecEnv}
importedSecrets={computeImportedSecretRows(
importSecEnv,
impSecPath,
importedSecrets,
secrets,
environments
)}
onDelete={onSecretImportDelete}
importedSecPath={impSecPath}
/>
))}
</SortableContext>
);
};
return (
<SortableContext items={items} strategy={verticalListSortingStrategy}>
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
<SecretImportItem
key={id}
importedEnv={importSecEnv}
importedSecrets={computeImportedSecretRows(
importSecEnv,
impSecPath,
importedSecrets,
secrets,
environments
)}
onDelete={onSecretImportDelete}
importedSecPath={impSecPath}
/>
))}
</SortableContext>
);
}
);
SecretImportSection.displayName = "SecretImportSection";

View File

@@ -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>
);
};

View File

@@ -1,6 +1,13 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { memo, useEffect, useRef } from "react";
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
import { memo, useEffect, useRef, useState } from "react";
import {
Control,
Controller,
useFieldArray,
UseFormRegister,
UseFormSetValue,
useWatch
} from "react-hook-form";
import {
faCheck,
faCodeBranch,
@@ -28,6 +35,7 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
SecretInput,
Tag,
TextArea,
Tooltip
@@ -36,24 +44,6 @@ import { useToggle } from "@app/hooks";
import { WsTag } from "@app/hooks/api/types";
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 = [
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
@@ -66,6 +56,31 @@ const tagColors = [
{ 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(
({
index,
@@ -77,10 +92,15 @@ export const SecretInputRow = memo(
wsTags,
onCreateTagOpen,
onSecretDelete,
searchTerm
searchTerm,
control,
register,
setValue,
isKeyError,
keyError,
secUniqId
}: Props): JSX.Element => {
const isKeySubDisabled = useRef<boolean>(false);
const { register, setValue, control } = useFormContext<FormData>();
// comment management in a row
const {
fields: secretTags,
@@ -89,28 +109,40 @@ export const SecretInputRow = memo(
} = useFieldArray({ control, name: `secrets.${index}.tags` });
// to get details on a secret
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` });
const secComment = useWatch({ control, name: `secrets.${index}.comment` });
const overrideAction = useWatch({
control,
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 secKey = useWatch({
control,
name: `secrets.${index}.key`,
disabled: isKeySubDisabled.current
disabled: isKeySubDisabled.current,
exact: true
});
const secValue = useWatch({
control,
name: `secrets.${index}.value`,
disabled: isKeySubDisabled.current
disabled: isKeySubDisabled.current,
exact: true
});
const secValueOverride = useWatch({
control,
name: `secrets.${index}.valueOverride`,
disabled: isKeySubDisabled.current
})
const secId = useWatch({ control, name: `secrets.${index}._id` });
disabled: isKeySubDisabled.current,
exact: true
});
// 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>>(
(prev, curr) => ({ ...prev, [curr.slug]: true }),
{}
@@ -126,15 +158,15 @@ export const SecretInputRow = memo(
return () => clearTimeout(timer);
}, [isInviteLinkCopied]);
useEffect(() => {
setEditorRef(isOverridden ? secValueOverride : secValue);
}, [isOverridden]);
const copyTokenToClipboard = () => {
navigator.clipboard.writeText((secValueOverride || secValue) as string);
setInviteLinkCopied.on();
};
// when secret is override by personal values
const isOverridden =
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
const onSecretOverride = () => {
if (isOverridden) {
// when user created a new override but then removes
@@ -193,10 +225,10 @@ export const SecretInputRow = memo(
control={control}
defaultValue=""
name={`secrets.${index}.key`}
render={({ fieldState: { error }, field }) => (
<HoverCard openDelay={0} open={error?.message ? undefined : false}>
render={({ field }) => (
<HoverCard openDelay={0} open={isKeyError ? undefined : false}>
<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]">
<Input
autoComplete="off"
@@ -220,21 +252,70 @@ export const SecretInputRow = memo(
<div>
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
</div>
<div className="text-sm">{error?.message}</div>
<div className="text-sm">{keyError}</div>
</div>
</HoverCardContent>
</HoverCard>
)}
/>
<td className="flex w-full flex-grow flex-row border-r border-none border-red">
<MaskedInput
isReadOnly={
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
}
isOverridden={isOverridden}
isSecretValueHidden={isSecretValueHidden}
index={index}
/>
<td
className="flex w-full flex-grow flex-row border-r border-none border-red"
style={{ padding: "0.5rem 0 0.5rem 1rem" }}
>
<div className="w-full">
{isOverridden ? (
<Controller
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 className="min-w-sm flex">
<div className="flex h-8 items-center pl-2">
@@ -251,7 +332,7 @@ export const SecretInputRow = memo(
{slug}
</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">
<IconButton
variant="plain"
@@ -396,7 +477,7 @@ export const SecretInputRow = memo(
size="lg"
colorSchema="primary"
variant="plain"
onClick={onRowExpand}
onClick={() => onRowExpand(secUniqId, index)}
ariaLabel="expand"
>
<FontAwesomeIcon icon={faEllipsis} />

View File

@@ -1,19 +1,326 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
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 { 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 = () => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
const { currentWorkspace, isLoading } = useWorkspace();
const { currentWorkspace, isLoading: isWorkspaceLoading } = 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;
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>
);
};

View File

@@ -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>
);
};

View File

@@ -0,0 +1 @@
export { FolderBreadCrumbs } from "./FolderBreadCrumbs";

View File

@@ -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>
);
};

View File

@@ -0,0 +1 @@
export { SecretOverviewFolderRow } from "./SecretOverviewFolderRow";

View File

@@ -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>
);
};

View File

@@ -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>
)}
</>
);
};

View File

@@ -0,0 +1 @@
export { SecretOverviewTableRow } from "./SecretOverviewTableRow";