diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5853ff6500..d25ff5cf17 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 0430844e72..51028a59b9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index de4adfff42..9c1afd84df 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -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"; diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx deleted file mode 100644 index 05dfe938bc..0000000000 --- a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx +++ /dev/null @@ -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> = {}; - 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 = 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 = { ...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; - // root condition - if (index === 0) delete query.secretPath; - router.push({ - pathname: router.pathname, - query - }); - }; - - if (isSecretsLoading || isEnvListLoading) { - return ( -
- loading animation -
- ); - } - - 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 ( -
-
- -
-
-

Secrets Overview

-

- Inject your secrets using - - Infisical CLI - - or - - Infisical SDKs - -

-
-
-
-
onFolderCrumbClick(0)} - onKeyDown={() => null} - role="button" - tabIndex={0} - > - -
- {(secretPath || "") - .split("/") - .filter(Boolean) - .map((path, index, arr) => ( -
onFolderCrumbClick(index + 1)} - onKeyDown={() => null} - role="button" - tabIndex={0} - > - {path} -
- ))} -
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> -
-
-
-
-
-
{0}
-
-
-
-
Secret
-
-
- {numSecretsMissingPerEnv && - userAvailableEnvs?.map((env) => { - return ( -
-
- {env.name} - {numSecretsMissingPerEnv[env.slug] > 0 && ( -
- - - {numSecretsMissingPerEnv[env.slug]} - - -
- )} -
-
- ); - })} -
-
- {!isDashboardEmpty && ( - - - - {Object.keys(foldersGroupedByEnv || {}).map((folderName, index) => ( - - ))} - {Object.keys(secrets?.secrets || {}) - ?.filter((secret: any) => - secret.toUpperCase().includes(searchFilter.toUpperCase()) - ) - .map((key) => ( - - ))} - -
-
- )} - {isDashboardEmpty && ( -
-
- - No secrets/folders found. - To add more secrets you can explore any environment. -
-
- )} -
-
-
-
0
-
-
- 0 - -
- {userAvailableEnvs?.map((env) => { - return ( -
- -
- ); - })} -
-
-
- ); -}; diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx index 262fb2a922..f991df9018 100644 --- a/frontend/src/views/DashboardPage/DashboardPage.tsx +++ b/frontend/src/views/DashboardPage/DashboardPage.tsx @@ -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(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 (
- -
- {/* breadcrumb row */} -
- envir.slug === envFromTop)[0].name || "" - } - isFolderMode - folders={folderData?.dir} - isProjectRelated - userAvailableEnvs={userAvailableEnvs} - onEnvChange={onEnvChange} + + {/* breadcrumb row */} +
+ envir.slug === envQuery)[0].name || ""} + isFolderMode + folders={folderData?.dir} + isProjectRelated + userAvailableEnvs={userAvailableEnvs} + onEnvChange={onEnvChange} + /> +
+
+
{isRollbackMode ? "Secret Snapshot" : ""}
+ {isRollbackMode && Boolean(snapshotSecret) && ( + + {new Date(snapshotSecret?.createdAt || "").toLocaleString()} + + )} +
+ {/* Environment, search and other action row */} +
+
+ setSearchFilter(e.target.value)} + leftIcon={} />
-
-
{isRollbackMode ? "Secret Snapshot" : ""}
- {isRollbackMode && Boolean(snapshotSecret) && ( - - {new Date(snapshotSecret?.createdAt || "").toLocaleString()} - - )} -
- {/* Environment, search and other action row */} -
-
- setSearchFilter(e.target.value)} - leftIcon={} - /> +
+
+ + + + + + + +
+ +
+
+
-
-
- - - - - - - -
- -
-
-
-
-
- - setIsSecretValueHidden.toggle()} - > - - - -
-
- - handlePopUpOpen("secretSnapshots")} - > - - - -
-
- -
- {!isReadOnly && !isRollbackMode && ( -
- - - -
- -
-
- -
-
- -
-
- -
-
-
-
-
- )} - {isRollbackMode && ( -
+
+ + handlePopUpOpen("secretSnapshots")} > - Go back - - )} + + + +
+
-
-
- {!isEmptyPage && ( - - - - - - - handlePopUpOpen("deleteSecretImport", { - environment: impSecEnv, - secretPath: impSecPath - }) - } - secrets={secrets?.secrets} - importedSecrets={importedSecrets} - items={items} - /> - handlePopUpOpen("folderForm", { id, name })} - onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })} - folders={folderList} - search={searchFilter} - /> - {fields.map(({ id, _id }, index) => ( - onDrawerOpen({ id: _id as string, index })} - isSecretValueHidden={isSecretValueHidden} - wsTags={wsTags} - onCreateTagOpen={() => handlePopUpOpen("addTag")} - /> - ))} - {!isReadOnly && !isRollbackMode && ( - - - - )} - -
- -
-
-
+ {!isReadOnly && !isRollbackMode && ( +
+ + + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
)} + {isRollbackMode && ( + + )} + +
+
+
+ {!isEmptyPage && ( + + + + + + + + {fields.map(({ id, _id }, index) => ( + + ))} + {!isReadOnly && !isRollbackMode && ( + + + + )} + +
+ +
+
+
+ )} + 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)} /> - -
- {/* secrets table and drawers, modals */} - - {/* Create a new tag modal */} - { - handlePopUpToggle("addTag", open); - }} + + + +
+ {/* secrets table and drawers, modals */} + + {/* Create a new tag modal */} + { + handlePopUpToggle("addTag", open); + }} + > + - - - - - {/* Uploaded env override or not confirmation modal */} - handlePopUpToggle("uploadedSecOpts", open)} + + + + {/* Uploaded env override or not confirmation modal */} + handlePopUpToggle("uploadedSecOpts", open)} + > + handlePopUpClose("uploadedSecOpts")} + > + Keep old + , + + ]} > - handlePopUpClose("uploadedSecOpts")} - > - Keep old - , - - ]} - > -
-
Your file contains following duplicate secrets
-
- {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) - ?.map((key) => key) - .join(", ")} -
-
Are you sure you want to overwrite these secrets?
+
+
Your file contains following duplicate secrets
+
+ {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {}) + ?.map((key) => key) + .join(", ")}
- - - handlePopUpToggle("folderForm", isOpen)} +
Are you sure you want to overwrite these secrets?
+
+ + + handlePopUpToggle("folderForm", isOpen)} + > + + + + + handlePopUpToggle("addSecretImport", isOpen)} + > + - - - - - handlePopUpToggle("addSecretImport", isOpen)} + + + + handlePopUpToggle("deleteFolder", isOpen)} + onDeleteApproved={handleFolderDelete} + /> + handlePopUpToggle("deleteSecretImport", isOpen)} + onDeleteApproved={handleSecretImportDelete} + /> + handlePopUpToggle("compareSecrets", open)} + > + - - - - - handlePopUpToggle("deleteFolder", isOpen)} - onDeleteApproved={handleFolderDelete} - /> - handlePopUpToggle("deleteSecretImport", isOpen)} - onDeleteApproved={handleSecretImportDelete} - /> - handlePopUpToggle("compareSecrets", open)} - > - - - - - + + + {subscription && ( ; export type TSecretDetailsOpen = { index: number; id: string }; export type TSecOverwriteOpt = { secrets: Record }; -export const downloadSecret = (secrets: FormData["secrets"] = [], env: string = "unknown") => { - const finalSecret = secrets.map(({ key, value, valueOverride, overrideAction, comment }) => ({ - key, - value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value, - comment - })); +export const downloadSecret = ( + secrets: FormData["secrets"] = [], + importedSecrets: { key: string; value?: string; comment?: string }[] = [], + env: string = "unknown" +) => { + const importSecPos: Record = {}; + 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 }) => { diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx deleted file mode 100644 index 6c86b3fce6..0000000000 --- a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx +++ /dev/null @@ -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(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 ( - -
-