From 9bc68b1cdc8544ce9a0c4463b8bdd42a458e82d4 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 15 Jul 2022 18:40:02 +0500 Subject: [PATCH 01/29] chore: update swagger docs --- api/public/swagger.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index ec7814c..5acf997 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -1709,13 +1709,13 @@ servers: tags: - name: Info - description: 'Get Server Info' + description: 'Get Server Information' - name: Session description: 'Get Session information' - name: User - description: 'Operations about users' + description: 'Operations with users' - name: Permission description: 'Operations about permissions' @@ -1727,16 +1727,16 @@ tags: description: 'Operations about auth' - name: Drive - description: 'Operations about drive' + description: 'Operations on SASjs Drive' - name: Group - description: 'Operations about group' + description: 'Operations on groups and group memberships' - name: STP - description: 'Operations about STP' + description: 'Execution of Stored Programs' - name: CODE - description: 'Operations on SAS code' + description: 'Execution of code (various runtimes are supported)' - name: Web description: 'Operations on Web' From 5e930f14d290edc5ab73cacc0a0fbbfea2d9384e Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 15 Jul 2022 18:41:11 +0500 Subject: [PATCH 02/29] chore: bump mui/icons-material and react-router-dom versions --- web/package-lock.json | 241 +++++++++++------------------------------- web/package.json | 4 +- 2 files changed, 62 insertions(+), 183 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index dbfbda5..41f181a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "@mui/icons-material": "^5.0.3", + "@mui/icons-material": "^5.8.4", "@mui/lab": "^5.0.0-alpha.50", "@mui/material": "^5.0.3", "@mui/styles": "^5.0.1", @@ -27,7 +27,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^17.0.2", "react-monaco-editor": "^0.48.0", - "react-router-dom": "^5.3.0", + "react-router-dom": "^6.3.0", "react-toastify": "^9.0.1" }, "devDependencies": { @@ -1836,9 +1836,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", - "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz", + "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==", "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -2312,19 +2312,23 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz", - "integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==", + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz", + "integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==", "dependencies": { - "@babel/runtime": "^7.16.0" + "@babel/runtime": "^7.17.2" }, "engines": { "node": ">=12.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, "peerDependencies": { "@mui/material": "^5.0.0", - "@types/react": "^16.8.6 || ^17.0.0", - "react": "^17.0.2" + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -7128,16 +7132,11 @@ } }, "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "@babel/runtime": "^7.7.6" } }, "node_modules/hoist-non-react-statics": { @@ -7829,11 +7828,6 @@ "node": ">=8" } }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8392,19 +8386,6 @@ "node": ">=4" } }, - "node_modules/mini-create-react-context": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "dependencies": { - "@babel/runtime": "^7.12.1", - "tiny-warning": "^1.0.3" - }, - "peerDependencies": { - "prop-types": "^15.0.0", - "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -8967,14 +8948,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dependencies": { - "isarray": "0.0.1" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9362,47 +9335,29 @@ "react": "^17.x" } }, - "node_modules/react-router": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", - "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, "node_modules/react-router-dom": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", - "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.2.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.2.0", + "react-router": "6.3.0" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/react-router/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "node_modules/react-router-dom/node_modules/react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "dependencies": { + "history": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.8" + } }, "node_modules/react-toastify": { "version": "9.0.1", @@ -9679,11 +9634,6 @@ "node": ">=4" } }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -10349,11 +10299,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "node_modules/tiny-invariant": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", - "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" - }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -10733,11 +10678,6 @@ "node": ">= 0.10" } }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -12642,9 +12582,9 @@ } }, "@babel/runtime": { - "version": "7.16.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", - "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.6.tgz", + "integrity": "sha512-t9wi7/AW6XtKahAe20Yw0/mMljKq0B1r2fPdvaAdV/KPDZewFXdaaa6K7lxmZBZ8FBNpCiAT6iHPmd6QO9bKfQ==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -12989,11 +12929,11 @@ } }, "@mui/icons-material": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.0.tgz", - "integrity": "sha512-GD2cNZ2XTqoxX6DMUg+tos1fDUVg6kXWxwo9UuBiRIhK8N+B7CG7vjRDf28LLmewcqIjxqy+T2SEVqDLy1FOYQ==", + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.8.4.tgz", + "integrity": "sha512-9Z/vyj2szvEhGWDvb+gG875bOGm8b8rlHBKOD1+nA3PcgC3fV6W1AU6pfOorPeBfH2X4mb9Boe97vHvaSndQvA==", "requires": { - "@babel/runtime": "^7.16.0" + "@babel/runtime": "^7.17.2" } }, "@mui/lab": { @@ -16587,16 +16527,11 @@ "dev": true }, "history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", "requires": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" + "@babel/runtime": "^7.7.6" } }, "hoist-non-react-statics": { @@ -17084,11 +17019,6 @@ "is-docker": "^2.0.0" } }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -17530,15 +17460,6 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" }, - "mini-create-react-context": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz", - "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==", - "requires": { - "@babel/runtime": "^7.12.1", - "tiny-warning": "^1.0.3" - } - }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -17961,14 +17882,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "requires": { - "isarray": "0.0.1" - } - }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -18260,44 +18173,25 @@ "prop-types": "^15.8.1" } }, - "react-router": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz", - "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==", + "react-router-dom": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz", + "integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==", "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "mini-create-react-context": "^0.4.0", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "history": "^5.2.0", + "react-router": "6.3.0" }, "dependencies": { - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "react-router": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", + "integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==", + "requires": { + "history": "^5.2.0" + } } } }, - "react-router-dom": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", - "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", - "requires": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.2.1", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - } - }, "react-toastify": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.1.tgz", @@ -18520,11 +18414,6 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, - "resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" - }, "retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -19026,11 +18915,6 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "tiny-invariant": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", - "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" - }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -19320,11 +19204,6 @@ "homedir-polyfill": "^1.0.1" } }, - "value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/web/package.json b/web/package.json index 8f6681d..95b1874 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,7 @@ "dependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "@mui/icons-material": "^5.0.3", + "@mui/icons-material": "^5.8.4", "@mui/lab": "^5.0.0-alpha.50", "@mui/material": "^5.0.3", "@mui/styles": "^5.0.1", @@ -26,7 +26,7 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^17.0.2", "react-monaco-editor": "^0.48.0", - "react-router-dom": "^5.3.0", + "react-router-dom": "^6.3.0", "react-toastify": "^9.0.1" }, "devDependencies": { From 30d7a653580b3da911636ea683ee6a3d6d32bfba Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 15 Jul 2022 18:42:59 +0500 Subject: [PATCH 03/29] chore: fix breaking changes caused by react-router-dom update --- web/src/App.tsx | 34 +++++++++++----------------------- web/src/components/header.tsx | 6 +++--- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 071a8a7..4ac2351 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ import React, { useContext } from 'react' -import { Route, HashRouter, Switch } from 'react-router-dom' +import { Route, HashRouter, Routes } from 'react-router-dom' import { ThemeProvider } from '@mui/material/styles' import { theme } from './theme' @@ -22,11 +22,9 @@ function App() {
- - - - - + + } /> + ) @@ -36,23 +34,13 @@ function App() {
- - - - - - - - - - - - - - - - - + + } /> + } /> + } /> + } /> + } /> + diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index 35c04ef..d80d01a 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useContext } from 'react' -import { Link, useHistory, useLocation } from 'react-router-dom' +import { Link, useNavigate, useLocation } from 'react-router-dom' import { AppBar, @@ -24,7 +24,7 @@ const baseUrl = const validTabs = ['/', '/SASjsDrive', '/SASjsStudio'] const Header = (props: any) => { - const history = useHistory() + const navigate = useNavigate() const { pathname } = useLocation() const appContext = useContext(AppContext) const [tabValue, setTabValue] = useState( @@ -74,7 +74,7 @@ const Header = (props: any) => { }} onClick={() => { setTabValue('/') - history.push('/') + navigate('/') }} /> Date: Mon, 18 Jul 2022 22:32:10 +0500 Subject: [PATCH 04/29] chore: add custom tree view component --- web/src/components/tree.tsx | 138 ++++++++++++++++++++++++++++++++++++ web/src/index.css | 12 ++++ web/src/utils/types.ts | 6 ++ 3 files changed, 156 insertions(+) create mode 100644 web/src/components/tree.tsx diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx new file mode 100644 index 0000000..e728b51 --- /dev/null +++ b/web/src/components/tree.tsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from 'react' +import { Menu, MenuItem } from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' + +import { TreeNode } from '../utils/types' + +type TreeViewProps = { + node: TreeNode + selectedFilePath: string + handleSelect: (filePath: string) => void + defaultExpanded?: string[] +} + +const TreeView = ({ + node, + selectedFilePath, + handleSelect, + defaultExpanded +}: TreeViewProps) => { + return ( +
    + +
+ ) +} + +export default TreeView + +type TreeViewNodeProps = { + node: TreeNode + selectedFilePath: string + handleSelect: (filePath: string) => void + defaultExpanded?: string[] +} + +const TreeViewNode = ({ + node, + selectedFilePath, + handleSelect, + defaultExpanded +}: TreeViewNodeProps) => { + const [childVisible, setChildVisibility] = useState(false) + const [contextMenu, setContextMenu] = useState<{ + mouseX: number + mouseY: number + } | null>(null) + + const handleContextMenu = (event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() + setContextMenu( + contextMenu === null + ? { + mouseX: event.clientX + 2, + mouseY: event.clientY - 6 + } + : null + ) + } + + const hasChild = node.children.length ? true : false + + const handleItemClick = () => { + if (node.children.length) { + setChildVisibility((v) => !v) + return + } + + if (!node.name.includes('.')) return + + handleSelect(node.relativePath) + } + + useEffect(() => { + if (defaultExpanded && defaultExpanded[0] === node.relativePath) { + setChildVisibility(true) + defaultExpanded.shift() + } + }, [defaultExpanded, node.relativePath]) + + return ( +
+
  • +
    handleItemClick()} + > + {hasChild && + (childVisible ? : )} +
    {node.name}
    +
    + + {hasChild && + childVisible && + node.children.map((child, index) => ( + + ))} +
  • + setContextMenu(null)} + anchorReference="anchorPosition" + anchorPosition={ + contextMenu !== null + ? { top: contextMenu.mouseY, left: contextMenu.mouseX } + : undefined + } + > + {hasChild && + ['Add Folder', 'Add File'].map((item) => ( + {item} + ))} + Rename + Delete + +
    + ) +} diff --git a/web/src/index.css b/web/src/index.css index 34d605c..6a506a5 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -25,3 +25,15 @@ code { padding: '5px 10px'; margin-top: '10px'; } + +.tree-item-label { + display: flex; +} + +.tree-item-label.selected { + background: lightgoldenrodyellow; +} + +.tree-item-label:hover { + background: lightgray; +} diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts index 4f0a80a..6d48d9b 100644 --- a/web/src/utils/types.ts +++ b/web/src/utils/types.ts @@ -30,3 +30,9 @@ export interface RegisterPermissionPayload { principalType: string principalId: number } + +export interface TreeNode { + name: string + relativePath: string + children: Array +} From 27410bc32b9cd1f5832d53b4a28fff8486355207 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 18 Jul 2022 22:37:32 +0500 Subject: [PATCH 05/29] chore: add file path input modal --- web/src/components/filePathInputModal.tsx | 70 +++++++++++++++++++++++ web/src/components/modal.tsx | 4 +- 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 web/src/components/filePathInputModal.tsx diff --git a/web/src/components/filePathInputModal.tsx b/web/src/components/filePathInputModal.tsx new file mode 100644 index 0000000..a75dedc --- /dev/null +++ b/web/src/components/filePathInputModal.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react' + +import { Button, DialogActions, DialogContent, TextField } from '@mui/material' + +import { BootstrapDialogTitle } from './dialogTitle' +import { BootstrapDialog } from './modal' + +type FilePathInputModalProps = { + open: boolean + setOpen: React.Dispatch> + saveFile: (filePath: string) => void +} + +const FilePathInputModal = ({ + open, + setOpen, + saveFile +}: FilePathInputModalProps) => { + const [filePath, setFilePath] = useState('') + const [hasError, setHasError] = useState(false) + const [errorText, setErrorText] = useState('') + + const handleChange = (event: React.ChangeEvent) => { + const value = event.target.value + const regex = /\.(exe|sh|htaccess)$/i + if (regex.test(value)) { + setHasError(true) + setErrorText('can not save file with extensions [exe, sh, htaccess]') + } else { + setHasError(false) + setErrorText('') + } + setFilePath(value) + } + + return ( + setOpen(false)} open={open}> + + Save File + + + + + + + + + + ) +} + +export default FilePathInputModal diff --git a/web/src/components/modal.tsx b/web/src/components/modal.tsx index 223bd9a..081d480 100644 --- a/web/src/components/modal.tsx +++ b/web/src/components/modal.tsx @@ -5,7 +5,7 @@ import { styled } from '@mui/material/styles' import { BootstrapDialogTitle } from './dialogTitle' -const BootstrapDialog = styled(Dialog)(({ theme }) => ({ +export const BootstrapDialog = styled(Dialog)(({ theme }) => ({ '& .MuiDialogContent-root': { padding: theme.spacing(2) }, @@ -14,7 +14,7 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({ } })) -export interface ModalProps { +type ModalProps = { open: boolean setOpen: React.Dispatch> title: string From 6c35412d2f5180d4e49b12e616576d8b8dacb7d8 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 18 Jul 2022 22:39:09 +0500 Subject: [PATCH 06/29] feat: add sidebar(drive) to left of studio --- web/src/containers/Studio/editor.tsx | 519 ++++++++++++++++++++++++++ web/src/containers/Studio/index.tsx | 298 ++++----------- web/src/containers/Studio/sideBar.tsx | 54 +++ 3 files changed, 641 insertions(+), 230 deletions(-) create mode 100644 web/src/containers/Studio/editor.tsx create mode 100644 web/src/containers/Studio/sideBar.tsx diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx new file mode 100644 index 0000000..4889282 --- /dev/null +++ b/web/src/containers/Studio/editor.tsx @@ -0,0 +1,519 @@ +import React, { useEffect, useRef, useState, useContext } from 'react' +import axios from 'axios' + +import { + Backdrop, + Box, + Button, + CircularProgress, + FormControl, + IconButton, + Menu, + MenuItem, + Paper, + Select, + SelectChangeEvent, + Tab, + Tooltip +} from '@mui/material' +import { styled } from '@mui/material/styles' + +import { RocketLaunch, MoreVert, Save, SaveAs } from '@mui/icons-material' +import Editor, { EditorDidMount } from 'react-monaco-editor' +import { TabContext, TabList, TabPanel } from '@mui/lab' + +import { AppContext, RunTimeType } from '../../context/appContext' + +import FilePathInputModal from '../../components/filePathInputModal' +import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar' +import Modal from '../../components/modal' + +const StyledTabPanel = styled(TabPanel)(() => ({ + padding: '10px' +})) + +const StyledTab = styled(Tab)(() => ({ + fontSize: '1rem', + color: 'gray', + '&.Mui-selected': { + color: 'black' + } +})) + +type SASjsEditorProps = { + selectedFilePath: string + setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void +} + +const baseUrl = window.location.origin + +const SASjsEditor = ({ + selectedFilePath, + setSelectedFilePath +}: SASjsEditorProps) => { + const appContext = useContext(AppContext) + const [isLoading, setIsLoading] = useState(false) + const [openModal, setOpenModal] = useState(false) + const [modalTitle, setModalTitle] = useState('') + const [modalPayload, setModalPayload] = useState('') + const [openSnackbar, setOpenSnackbar] = useState(false) + const [snackbarMessage, setSnackbarMessage] = useState('') + const [snackbarSeverity, setSnackbarSeverity] = useState( + AlertSeverityType.Success + ) + const [prevFileContent, setPrevFileContent] = useState('') + const [fileContent, setFileContent] = useState('') + const [log, setLog] = useState('') + const [ctrlPressed, setCtrlPressed] = useState(false) + const [webout, setWebout] = useState('') + const [tab, setTab] = useState('1') + const [runTimes, setRunTimes] = useState([]) + const [selectedRunTime, setSelectedRunTime] = useState('') + const [selectedFileExtension, setSelectedFileExtension] = useState('') + const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false) + + const editorRef = useRef(null as any) + + const handleEditorDidMount: EditorDidMount = (editor) => { + editor.focus() + editorRef.current = editor + } + + useEffect(() => { + setRunTimes(Object.values(appContext.runTimes)) + }, [appContext.runTimes]) + + useEffect(() => { + if (runTimes.length) setSelectedRunTime(runTimes[0]) + }, [runTimes]) + + useEffect(() => { + if (selectedFilePath) { + setIsLoading(true) + setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '') + axios + .get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`) + .then((res: any) => { + setPrevFileContent(res.data) + setFileContent(res.data) + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) + setOpenModal(true) + }) + .finally(() => setIsLoading(false)) + } + }, [selectedFilePath]) + + const handleTabChange = (_e: any, newValue: string) => { + setTab(newValue) + } + + const getSelection = () => { + const editor = editorRef.current as any + const selection = editor?.getModel().getValueInRange(editor?.getSelection()) + return selection ?? '' + } + + const handleRunBtnClick = () => runCode(getSelection() || fileContent) + + const runCode = (code: string) => { + setIsLoading(true) + axios + .post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime }) + .then((res: any) => { + const parsedLog = res?.data?.log + .map((logLine: any) => logLine.line) + .join('\n') + + setLog(parsedLog) + + setWebout(`${res.data?._webout}`) + setTab('2') + + // Scroll to bottom of log + window.scrollTo(0, document.body.scrollHeight) + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) + setOpenModal(true) + }) + .finally(() => setIsLoading(false)) + } + + const handleKeyDown = (event: any) => { + if (event.ctrlKey) { + if (event.key === 'v') { + setCtrlPressed(false) + } + + if (event.key === 'Enter') runCode(getSelection() || fileContent) + if (!ctrlPressed) setCtrlPressed(true) + } + } + + const handleKeyUp = (event: any) => { + if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false) + } + + const handleChangeRunTime = (event: SelectChangeEvent) => { + setSelectedRunTime(event.target.value as RunTimeType) + } + + const handleFilePathInput = (filePath: string) => { + setOpenFilePathInputModal(false) + saveFile(filePath) + } + + const saveFile = (filePath?: string) => { + setIsLoading(true) + + const formData = new FormData() + + const stringBlob = new Blob([fileContent], { type: 'text/plain' }) + formData.append('file', stringBlob, 'filename.sas') + formData.append('filePath', filePath ?? selectedFilePath) + + const axiosPromise = filePath + ? axios.post('/SASjsApi/drive/file', formData) + : axios.patch('/SASjsApi/drive/file', formData) + + axiosPromise + .then(() => { + if (filePath) { + setSelectedFilePath(filePath, true) + } + setPrevFileContent(fileContent) + setSnackbarMessage('File saved!') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) + setOpenModal(true) + }) + .finally(() => { + setIsLoading(false) + }) + } + + return ( + + theme.zIndex.drawer + 1 }} + open={isLoading} + > + + + {selectedFilePath && !runTimes.includes(selectedFileExtension) ? ( + + + + + + setFileContent(val)} + /> + + + ) : ( + + + + + + + + + + + + + + + + + + setFileContent(val)} + /> +

    + Press CTRL + ENTER to run code +

    +
    +
    + +
    +

    SAS Log

    +
    {log}
    +
    +
    + +
    +
    {webout}
    +
    +
    +
    + )} + + + +
    + ) +} + +export default SASjsEditor + +type RunMenuProps = { + selectedFilePath: string + selectedRunTime: string + runTimes: string[] + handleChangeRunTime: (event: SelectChangeEvent) => void + handleRunBtnClick: () => void +} + +const RunMenu = ({ + selectedFilePath, + selectedRunTime, + runTimes, + handleChangeRunTime, + handleRunBtnClick +}: RunMenuProps) => { + const launchProgram = () => { + window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`) + } + + return ( + <> + + + + {selectedFilePath ? ( + + + + + + + + ) : ( + + + + + + )} + + ) +} + +type FileMenuProps = { + prevFileContent: string + currentFileContent: string + selectedFilePath: string + setOpenFilePathInputModal: React.Dispatch> + saveFile: () => void +} + +const FileMenu = ({ + prevFileContent, + currentFileContent, + selectedFilePath, + setOpenFilePathInputModal, + saveFile +}: FileMenuProps) => { + const [anchorEl, setAnchorEl] = useState< + (EventTarget & HTMLButtonElement) | null + >(null) + + const handleMenu = ( + event?: React.MouseEvent + ) => { + if (event) setAnchorEl(event.currentTarget) + else setAnchorEl(null) + } + + const handleSaveAsBtnClick = () => { + setAnchorEl(null) + setOpenFilePathInputModal(true) + } + + const handleSaveBtnClick = () => { + setAnchorEl(null) + saveFile() + } + + return ( + <> + + + + + + handleMenu()} + > + + + + + + + + + ) +} diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index 011ce05..5d556ca 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -1,253 +1,91 @@ -import React, { useEffect, useRef, useState, useContext } from 'react' +import React, { useState, useEffect, useCallback } from 'react' +import { useSearchParams } from 'react-router-dom' import axios from 'axios' -import { - Backdrop, - Box, - Button, - CircularProgress, - FormControl, - MenuItem, - Paper, - Select, - SelectChangeEvent, - Tab, - Tooltip -} from '@mui/material' -import { makeStyles } from '@mui/styles' -import Editor, { EditorDidMount } from 'react-monaco-editor' -import { useLocation } from 'react-router-dom' -import { TabContext, TabList, TabPanel } from '@mui/lab' +import CssBaseline from '@mui/material/CssBaseline' +import Box from '@mui/material/Box' -import { AppContext, RunTimeType } from '../../context/appContext' +import { TreeNode } from '../../utils/types' -const useStyles = makeStyles(() => ({ - root: { - fontSize: '1rem', - color: 'gray', - '&.Mui-selected': { - color: 'black' - } - }, - subMenu: { - marginTop: '25px', - display: 'flex', - justifyContent: 'center' - }, - runButton: { - display: 'flex', - alignItems: 'center', - padding: '5px 5px', - minWidth: 'unset' - } -})) +import SideBar from './sideBar' +import SASjsEditor from './editor' const Studio = () => { - const appContext = useContext(AppContext) - const location = useLocation() - const [fileContent, setFileContent] = useState('') - const [log, setLog] = useState('') - const [ctrlPressed, setCtrlPressed] = useState(false) - const [webout, setWebout] = useState('') - const [tab, setTab] = useState('1') - const [runTimes, setRunTimes] = useState([]) - const [selectedRunTime, setSelectedRunTime] = useState('') - const [isRunning, setIsRunning] = useState(false) + const [searchParams, setSearchParams] = useSearchParams() + const [selectedFilePath, setSelectedFilePath] = useState('') + const [directoryData, setDirectoryData] = useState(null) useEffect(() => { - setRunTimes(Object.values(appContext.runTimes)) - }, [appContext.runTimes]) + setSelectedFilePath(searchParams.get('filePath') ?? '') + }, [searchParams]) - useEffect(() => { - if (runTimes.length) setSelectedRunTime(runTimes[0]) - }, [runTimes]) - - const handleTabChange = (_e: any, newValue: string) => { - setTab(newValue) - } - - const editorRef = useRef(null as any) - const handleEditorDidMount: EditorDidMount = (editor) => { - editor.focus() - editorRef.current = editor - } - - const getSelection = () => { - const editor = editorRef.current as any - const selection = editor?.getModel().getValueInRange(editor?.getSelection()) - return selection ?? '' - } - - const handleRunBtnClick = () => runCode(getSelection() || fileContent) - - const runCode = (code: string) => { - setIsRunning(true) + const fetchDirectoryData = useCallback(() => { axios - .post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime }) + .get(`/SASjsApi/drive/fileTree`) .then((res: any) => { - const parsedLog = res?.data?.log - .map((logLine: any) => logLine.line) - .join('\n') - - setLog(parsedLog) - - setWebout(`${res.data?._webout}`) - setTab('2') - - // Scroll to bottom of log - window.scrollTo(0, document.body.scrollHeight) + if (res.data && res.data?.status === 'success') { + setDirectoryData(res.data.tree) + } + }) + .catch((err) => { + console.log(err) }) - .catch((err) => console.log(err)) - .finally(() => setIsRunning(false)) - } - - const handleKeyDown = (event: any) => { - if (event.ctrlKey) { - if (event.key === 'v') { - setCtrlPressed(false) - } - - if (event.key === 'Enter') runCode(getSelection() || fileContent) - if (!ctrlPressed) setCtrlPressed(true) - } - } - - const handleKeyUp = (event: any) => { - if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false) - } - - const handleChangeRunTime = (event: SelectChangeEvent) => { - setSelectedRunTime(event.target.value as RunTimeType) - } - - useEffect(() => { - const content = localStorage.getItem('fileContent') ?? '' - setFileContent(content) }, []) useEffect(() => { - if (fileContent.length) { - localStorage.setItem('fileContent', fileContent) + fetchDirectoryData() + }, [fetchDirectoryData]) + + const handleSelect = (filePath: string, refreshSideBar?: boolean) => { + setSearchParams({ filePath }) + if (refreshSideBar) fetchDirectoryData() + } + + const removeFileFromTree = (path: string) => { + if (directoryData) { + const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode + findAndRemoveNode(newTree, newTree, path) + setDirectoryData(newTree) } - }, [fileContent]) + } - useEffect(() => { - const params = new URLSearchParams(location.search) - const programPath = params.get('_program') + const findAndRemoveNode = ( + node: TreeNode, + parentNode: TreeNode, + path: string + ) => { + if (node.relativePath === path) { + removeNodeFromParent(parentNode, path) + return true + } + if (Array.isArray(node.children)) { + for (let i = 0; i < node.children.length; i++) { + if (findAndRemoveNode(node.children[i], node, path)) return + } + } + } - if (programPath?.length) - axios - .get(`/SASjsApi/drive/file?filePath=${programPath}`) - .then((res: any) => setFileContent(res.data.fileContent)) - .catch((err) => console.log(err)) - }, [location.search]) - - const classes = useStyles() + const removeNodeFromParent = (parent: TreeNode, path: string) => { + const index = parent.children.findIndex( + (node) => node.relativePath === path + ) + if (index !== -1) { + parent.children.splice(index, 1) + } + } return ( - - - - - - - - - - - - - - theme.zIndex.drawer + 1 }} - open={isRunning} - > - - -
    - - - - - - - - -
    - - { - if (val) setFileContent(val) - }} - /> -

    - Press CTRL + ENTER to run SAS code -

    -
    -
    - -
    -

    SAS Log

    -
    {log}
    -
    -
    - -
    -
    {webout}
    -
    -
    -
    + + + + ) } diff --git a/web/src/containers/Studio/sideBar.tsx b/web/src/containers/Studio/sideBar.tsx new file mode 100644 index 0000000..9f18e08 --- /dev/null +++ b/web/src/containers/Studio/sideBar.tsx @@ -0,0 +1,54 @@ +import React, { useMemo } from 'react' + +import { Box, Drawer, Toolbar } from '@mui/material' + +import TreeView from '../../components/tree' +import { TreeNode } from '../../utils/types' + +const drawerWidth = 240 + +type Props = { + selectedFilePath: string + directoryData: TreeNode | null + handleSelect: (filePath: string) => void +} + +const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => { + const defaultExpanded = useMemo(() => { + const splittedPath = selectedFilePath.split('/') + const arr = [''] + let nodeId = '' + splittedPath.forEach((path) => { + if (path !== '') { + nodeId += '/' + path + arr.push(nodeId) + } + }) + return arr + }, [selectedFilePath]) + + return ( + + + + {directoryData && ( + + )} + + + ) +} + +export default SideBar From 0f193849994f1ac8a071afa8f10af5b46f86663d Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 19 Jul 2022 16:13:46 +0500 Subject: [PATCH 07/29] fix: fileTree api response to include an additional attribute isFolder --- api/src/controllers/internal/Execution.ts | 10 +++++++++- api/src/types/TreeNode.ts | 1 + web/src/components/tree.tsx | 2 +- web/src/utils/types.ts | 1 + 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 7f3541d..1beb046 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -143,6 +143,7 @@ export class ExecutionController { name: 'files', relativePath: '', absolutePath: getFilesFolder(), + isFolder: true, children: [] } @@ -152,15 +153,22 @@ export class ExecutionController { const currentNode = stack.pop() if (currentNode) { + currentNode.isFolder = fs + .statSync(currentNode.absolutePath) + .isDirectory() + const children = fs.readdirSync(currentNode.absolutePath) for (let child of children) { - const absoluteChildPath = `${currentNode.absolutePath}/${child}` + const absoluteChildPath = path.join(currentNode.absolutePath, child) + // relative path will only be used in frontend component + // so, no need to convert '/' to platform specific separator const relativeChildPath = `${currentNode.relativePath}/${child}` const childNode: TreeNode = { name: child, relativePath: relativeChildPath, absolutePath: absoluteChildPath, + isFolder: false, children: [] } currentNode.children.push(childNode) diff --git a/api/src/types/TreeNode.ts b/api/src/types/TreeNode.ts index 7d902ac..71fd803 100644 --- a/api/src/types/TreeNode.ts +++ b/api/src/types/TreeNode.ts @@ -2,5 +2,6 @@ export interface TreeNode { name: string relativePath: string absolutePath: string + isFolder: boolean children: Array } diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx index e728b51..f8c7376 100644 --- a/web/src/components/tree.tsx +++ b/web/src/components/tree.tsx @@ -126,7 +126,7 @@ const TreeViewNode = ({ : undefined } > - {hasChild && + {node.isFolder && ['Add Folder', 'Add File'].map((item) => ( {item} ))} diff --git a/web/src/utils/types.ts b/web/src/utils/types.ts index 6d48d9b..99c6fc4 100644 --- a/web/src/utils/types.ts +++ b/web/src/utils/types.ts @@ -34,5 +34,6 @@ export interface RegisterPermissionPayload { export interface TreeNode { name: string relativePath: string + isFolder: boolean children: Array } From 3e53f7092815d46c571272c7c3283f8a49086654 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 19 Jul 2022 16:14:40 +0500 Subject: [PATCH 08/29] chore: update swagger docs --- api/public/swagger.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 5acf997..cb785f3 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -248,6 +248,8 @@ components: type: string absolutePath: type: string + isFolder: + type: boolean children: items: $ref: '#/components/schemas/TreeNode' @@ -256,6 +258,7 @@ components: - name - relativePath - absolutePath + - isFolder - children type: object additionalProperties: false From 08e0c61e0fd7041d6cded6f4d71fbb410e5615ce Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 19 Jul 2022 22:41:03 +0500 Subject: [PATCH 09/29] feat: add api end point for delete folder --- api/public/swagger.yaml | 27 +++++++++++++++++++++++++++ api/src/controllers/drive.ts | 32 ++++++++++++++++++++++++++++++++ api/src/routes/api/drive.ts | 13 +++++++++++++ 3 files changed, 72 insertions(+) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index cb785f3..9a104b8 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -963,6 +963,33 @@ paths: schema: type: string example: /Public/somefolder + delete: + operationId: DeleteFolder + responses: + '200': + description: Ok + content: + application/json: + schema: + properties: + status: {type: string} + required: + - status + type: object + summary: 'Delete folder from SASjs Drive' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: + - + in: query + name: _folderPath + required: true + schema: + type: string + example: /Public/somefolder/ /SASjsApi/drive/filetree: get: operationId: GetFileTree diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index 3562311..358e276 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -22,6 +22,7 @@ import { moveFile, createFolder, deleteFile as deleteFileOnSystem, + deleteFolder as deleteFolderOnSystem, folderExists, listFilesInFolder, listSubFoldersInFolder, @@ -140,6 +141,17 @@ export class DriveController { return getFolder(_folderPath) } + /** + * + * @summary Delete folder from SASjs Drive + * @query _folderPath Location of folder + * @example _folderPath "/Public/somefolder/" + */ + @Delete('/folder') + public async deleteFolder(@Query() _folderPath: string) { + return deleteFolder(_folderPath) + } + /** * * @summary Delete file from SASjs Drive @@ -315,6 +327,26 @@ const deleteFile = async (filePath: string) => { return { status: 'success' } } +const deleteFolder = async (folderPath: string) => { + const driveFolderPath = getFilesFolder() + + const folderPathFull = path + .join(getFilesFolder(), folderPath) + .replace(new RegExp('/', 'g'), path.sep) + + if (!folderPathFull.includes(driveFolderPath)) { + throw new Error('Cannot delete file outside drive.') + } + + if (!(await fileExists(folderPathFull))) { + throw new Error('Folder does not exist.') + } + + await deleteFolderOnSystem(folderPathFull) + + return { status: 'success' } +} + const saveFile = async ( filePath: string, multerFile: Express.Multer.File diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 6126946..1f59271 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -149,6 +149,19 @@ driveRouter.delete('/file', async (req, res) => { } }) +driveRouter.delete('/folder', async (req, res) => { + const { error: errQ, value: query } = folderParamValidation(req.query) + + if (errQ) return res.status(400).send(errQ.details[0].message) + + try { + const response = await controller.deleteFolder(query._folderPath) + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + driveRouter.post( '/file', (...arg) => multerSingle('file', arg), From 721165ff12bdcbabe395dfbaa2e324c9cadba531 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 19 Jul 2022 22:48:22 +0500 Subject: [PATCH 10/29] chore: add delete confirmation modal and use it in permission component --- .../deleteConfirmationModal.tsx} | 21 ++++++++++------- web/src/containers/Settings/permission.tsx | 23 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) rename web/src/{containers/Settings/deletePermissionModal.tsx => components/deleteConfirmationModal.tsx} (62%) diff --git a/web/src/containers/Settings/deletePermissionModal.tsx b/web/src/components/deleteConfirmationModal.tsx similarity index 62% rename from web/src/containers/Settings/deletePermissionModal.tsx rename to web/src/components/deleteConfirmationModal.tsx index 23736f8..ec0eb0b 100644 --- a/web/src/containers/Settings/deletePermissionModal.tsx +++ b/web/src/components/deleteConfirmationModal.tsx @@ -18,22 +18,27 @@ const BootstrapDialog = styled(Dialog)(({ theme }) => ({ } })) -type DeleteModalProps = { +type DeleteConfirmationModalProps = { open: boolean setOpen: React.Dispatch> - deletePermission: () => void + message: string + _delete: () => void } -const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => { +const DeleteConfirmationModal = ({ + open, + setOpen, + message, + _delete +}: DeleteConfirmationModalProps) => { return ( setOpen(false)} open={open}> - - Are you sure you want to delete this permission? - + {message} - + @@ -41,4 +46,4 @@ const DeleteModal = ({ open, setOpen, deletePermission }: DeleteModalProps) => { ) } -export default DeleteModal +export default DeleteConfirmationModal diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index 81b2180..772d592 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -28,7 +28,7 @@ import Modal from '../../components/modal' import PermissionFilterModal from './permissionFilterModal' import AddPermissionModal from './addPermissionModal' import UpdatePermissionModal from './updatePermissionModal' -import DeleteModal from './deletePermissionModal' +import DeleteConfirmationModal from '../../components/deleteConfirmationModal' import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar' import { @@ -61,7 +61,10 @@ const Permission = () => { const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) const [updatePermissionModalOpen, setUpdatePermissionModalOpen] = useState(false) - const [deleteModalOpen, setDeleteModalOpen] = useState(false) + const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = + useState(false) + const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] = + useState('') const [selectedPermission, setSelectedPermission] = useState() const [filterModalOpen, setFilterModalOpen] = useState(false) @@ -236,11 +239,14 @@ const Permission = () => { const handleDeletePermissionClick = (permission: PermissionResponse) => { setSelectedPermission(permission) - setDeleteModalOpen(true) + setDeleteConfirmationModalOpen(true) + setDeleteConfirmationModalMessage( + 'Are you sure you want to delete this permission?' + ) } const deletePermission = () => { - setDeleteModalOpen(false) + setDeleteConfirmationModalOpen(false) setIsLoading(true) axios .delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`) @@ -338,10 +344,11 @@ const Permission = () => { permission={selectedPermission} updatePermission={updatePermission} /> -
    ) From 177675bc897416f7994dd849dc7bb11ba072efe9 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 19 Jul 2022 22:49:34 +0500 Subject: [PATCH 11/29] feat: implemented delete file/folder functionality --- web/src/components/tree.tsx | 49 +++++++++++++++++++++------ web/src/containers/Studio/editor.tsx | 2 ++ web/src/containers/Studio/index.tsx | 7 ++++ web/src/containers/Studio/sideBar.tsx | 25 ++++++++++++-- 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx index f8c7376..9b7db0f 100644 --- a/web/src/components/tree.tsx +++ b/web/src/components/tree.tsx @@ -3,12 +3,15 @@ import { Menu, MenuItem } from '@mui/material' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ChevronRightIcon from '@mui/icons-material/ChevronRight' +import DeleteConfirmationModal from './deleteConfirmationModal' + import { TreeNode } from '../utils/types' -type TreeViewProps = { +type Props = { node: TreeNode selectedFilePath: string handleSelect: (filePath: string) => void + deleteNode: (path: string, isFolder: boolean) => void defaultExpanded?: string[] } @@ -16,8 +19,9 @@ const TreeView = ({ node, selectedFilePath, handleSelect, + deleteNode, defaultExpanded -}: TreeViewProps) => { +}: Props) => { return (
    @@ -38,19 +43,17 @@ const TreeView = ({ export default TreeView -type TreeViewNodeProps = { - node: TreeNode - selectedFilePath: string - handleSelect: (filePath: string) => void - defaultExpanded?: string[] -} - const TreeViewNode = ({ node, selectedFilePath, handleSelect, + deleteNode, defaultExpanded -}: TreeViewNodeProps) => { +}: Props) => { + const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = + useState(false) + const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] = + useState('') const [childVisible, setChildVisibility] = useState(false) const [contextMenu, setContextMenu] = useState<{ mouseX: number @@ -90,6 +93,21 @@ const TreeViewNode = ({ } }, [defaultExpanded, node.relativePath]) + const handleDeleteItemClick = () => { + setContextMenu(null) + setDeleteConfirmationModalOpen(true) + setDeleteConfirmationModalMessage( + `Are you sure you want to delete ${node.isFolder ? 'folder' : 'file'} "${ + node.relativePath + }"?` + ) + } + + const deleteConfirm = () => { + setDeleteConfirmationModalOpen(false) + deleteNode(node.relativePath, node.isFolder) + } + return (
  • @@ -112,10 +130,17 @@ const TreeViewNode = ({ node={child} selectedFilePath={selectedFilePath} handleSelect={handleSelect} + deleteNode={deleteNode} defaultExpanded={defaultExpanded} /> ))}
  • + setContextMenu(null)} @@ -131,7 +156,9 @@ const TreeViewNode = ({ {item} ))} Rename - Delete + + Delete +
    ) diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx index 4889282..a3b0be0 100644 --- a/web/src/containers/Studio/editor.tsx +++ b/web/src/containers/Studio/editor.tsx @@ -107,6 +107,8 @@ const SASjsEditor = ({ setOpenModal(true) }) .finally(() => setIsLoading(false)) + } else { + setFileContent('') } }, [selectedFilePath]) diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index 5d556ca..1dd7051 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -56,6 +56,12 @@ const Studio = () => { ) => { if (node.relativePath === path) { removeNodeFromParent(parentNode, path) + // reset selected file path and file path query param + if ( + node.relativePath === selectedFilePath || + selectedFilePath.startsWith(node.relativePath) + ) + setSearchParams({}) return true } if (Array.isArray(node.children)) { @@ -81,6 +87,7 @@ const Studio = () => { selectedFilePath={selectedFilePath} directoryData={directoryData} handleSelect={handleSelect} + removeFileFromTree={removeFileFromTree} /> void + removeFileFromTree: (filePath: string) => void } -const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => { +const SideBar = ({ + selectedFilePath, + directoryData, + handleSelect, + removeFileFromTree +}: Props) => { const defaultExpanded = useMemo(() => { const splittedPath = selectedFilePath.split('/') const arr = [''] @@ -27,6 +33,20 @@ const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => { return arr }, [selectedFilePath]) + const deleteNode = (path: string, isFolder: boolean) => { + const axiosPromise = axios.delete( + `/SASjsApi/drive/${ + isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}` + }` + ) + + axiosPromise + .then(() => removeFileFromTree(path)) + .catch((err) => { + console.log(err) + }) + } + return ( { node={directoryData} selectedFilePath={selectedFilePath} handleSelect={handleSelect} + deleteNode={deleteNode} defaultExpanded={defaultExpanded} /> )} From 941917e508ece5009135f9dddf99775dd4002f78 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 20 Jul 2022 16:43:43 +0500 Subject: [PATCH 12/29] feat: created api endpoint for adding empty folder in drive --- api/public/swagger.yaml | 56 +++++++++++++++++++++--- api/src/controllers/drive.ts | 84 ++++++++++++++++++++++++++++-------- api/src/routes/api/drive.ts | 14 ++++++ api/src/utils/validation.ts | 5 +++ 4 files changed, 134 insertions(+), 25 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 9a104b8..b42c4b4 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -230,7 +230,7 @@ components: - fileTree type: object additionalProperties: false - UpdateFileResponse: + FileFolderResponse: properties: status: type: string @@ -240,6 +240,16 @@ components: - status type: object additionalProperties: false + AddFolderPayload: + properties: + folderPath: + type: string + description: 'Location of folder' + example: /Public/someFolder + required: + - folderPath + type: object + additionalProperties: false TreeNode: properties: name: @@ -839,7 +849,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UpdateFileResponse' + $ref: '#/components/schemas/FileFolderResponse' examples: 'Example 1': value: {status: success} @@ -848,7 +858,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UpdateFileResponse' + $ref: '#/components/schemas/FileFolderResponse' examples: 'Example 1': value: {status: failure, message: 'File request failed.'} @@ -861,7 +871,7 @@ paths: bearerAuth: [] parameters: - - description: 'Location of SAS program' + description: 'Location of file' in: query name: _filePath required: false @@ -890,7 +900,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UpdateFileResponse' + $ref: '#/components/schemas/FileFolderResponse' examples: 'Example 1': value: {status: success} @@ -899,7 +909,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UpdateFileResponse' + $ref: '#/components/schemas/FileFolderResponse' examples: 'Example 1': value: {status: failure, message: 'File request failed.'} @@ -990,6 +1000,40 @@ paths: schema: type: string example: /Public/somefolder/ + post: + operationId: AddFolder + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/FileFolderResponse' + examples: + 'Example 1': + value: {status: success} + '409': + description: 'Folder already exists' + content: + application/json: + schema: + $ref: '#/components/schemas/FileFolderResponse' + examples: + 'Example 1': + value: {status: failure, message: 'Add folder request failed.'} + summary: 'Create an empty folder in SASjs Drive' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddFolderPayload' /SASjsApi/drive/filetree: get: operationId: GetFileTree diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index 358e276..d99a4d2 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -59,11 +59,19 @@ interface GetFileTreeResponse { tree: TreeNode } -interface UpdateFileResponse { +interface FileFolderResponse { status: string message?: string } +interface AddFolderPayload { + /** + * Location of folder + * @example "/Public/someFolder" + */ + folderPath: string +} + const fileTreeExample = getTreeExample() const successDeployResponse: DeployResponse = { @@ -141,6 +149,17 @@ export class DriveController { return getFolder(_folderPath) } + /** + * + * @summary Delete file from SASjs Drive + * @query _filePath Location of file + * @example _filePath "/Public/somefolder/some.file" + */ + @Delete('/file') + public async deleteFile(@Query() _filePath: string) { + return deleteFile(_filePath) + } + /** * * @summary Delete folder from SASjs Drive @@ -152,31 +171,20 @@ export class DriveController { return deleteFolder(_folderPath) } - /** - * - * @summary Delete file from SASjs Drive - * @query _filePath Location of SAS program - * @example _filePath "/Public/somefolder/some.file" - */ - @Delete('/file') - public async deleteFile(@Query() _filePath: string) { - return deleteFile(_filePath) - } - /** * It's optional to either provide `_filePath` in url as query parameter * Or provide `filePath` in body as form field. * But it's required to provide else API will respond with Bad Request. * * @summary Create a file in SASjs Drive - * @param _filePath Location of SAS program + * @param _filePath Location of file * @example _filePath "/Public/somefolder/some.file.sas" * */ - @Example({ + @Example({ status: 'success' }) - @Response(403, 'File already exists', { + @Response(403, 'File already exists', { status: 'failure', message: 'File request failed.' }) @@ -185,10 +193,28 @@ export class DriveController { @UploadedFile() file: Express.Multer.File, @Query() _filePath?: string, @FormField() filePath?: string - ): Promise { + ): Promise { return saveFile((_filePath ?? filePath)!, file) } + /** + * @summary Create an empty folder in SASjs Drive + * + */ + @Example({ + status: 'success' + }) + @Response(409, 'Folder already exists', { + status: 'failure', + message: 'Add folder request failed.' + }) + @Post('/folder') + public async addFolder( + @Body() body: AddFolderPayload + ): Promise { + return addFolder(body.folderPath) + } + /** * It's optional to either provide `_filePath` in url as query parameter * Or provide `filePath` in body as form field. @@ -199,10 +225,10 @@ export class DriveController { * @example _filePath "/Public/somefolder/some.file.sas" * */ - @Example({ + @Example({ status: 'success' }) - @Response(403, `File doesn't exist`, { + @Response(403, `File doesn't exist`, { status: 'failure', message: 'File request failed.' }) @@ -211,7 +237,7 @@ export class DriveController { @UploadedFile() file: Express.Multer.File, @Query() _filePath?: string, @FormField() filePath?: string - ): Promise { + ): Promise { return updateFile((_filePath ?? filePath)!, file) } @@ -372,6 +398,26 @@ const saveFile = async ( return { status: 'success' } } +const addFolder = async (folderPath: string): Promise => { + const drivePath = getFilesFolder() + + const folderPathFull = path + .join(drivePath, folderPath) + .replace(new RegExp('/', 'g'), path.sep) + + if (!folderPathFull.includes(drivePath)) { + throw new Error('Cannot put folder outside drive.') + } + + if (await folderExists(folderPathFull)) { + throw new Error('Folder already exists.') + } + + await createFolder(folderPathFull) + + return { status: 'success' } +} + const updateFile = async ( filePath: string, multerFile: Express.Multer.File diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 1f59271..90fc707 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -11,6 +11,7 @@ import { extractName, fileBodyValidation, fileParamValidation, + folderBodyValidation, folderParamValidation, isZipFile } from '../../utils' @@ -190,6 +191,19 @@ driveRouter.post( } ) +driveRouter.post('/folder', async (req, res) => { + const { error, value: body } = folderBodyValidation(req.body) + + if (error) return res.status(400).send(error.details[0].message) + + try { + const response = await controller.addFolder(body) + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + driveRouter.patch( '/file', (...arg) => multerSingle('file', arg), diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 0789fa5..0dae10c 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -143,6 +143,11 @@ export const folderParamValidation = (data: any): Joi.ValidationResult => _folderPath: Joi.string() }).validate(data) +export const folderBodyValidation = (data: any): Joi.ValidationResult => + Joi.object({ + folderPath: Joi.string() + }).validate(data) + export const runCodeValidation = (data: any): Joi.ValidationResult => Joi.object({ code: Joi.string().required(), From 0ce94a553e53bfcdbd6273b26b322095a080a341 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 20 Jul 2022 16:45:45 +0500 Subject: [PATCH 13/29] feat: implemented functionality for adding file/folder from sidebar context menu --- web/src/components/nameInputModal.tsx | 82 ++++++++++++++++++++ web/src/components/tree.tsx | 49 +++++++++++- web/src/containers/Studio/index.tsx | 1 + web/src/containers/Studio/sideBar.tsx | 107 ++++++++++++++++++++++++-- 4 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 web/src/components/nameInputModal.tsx diff --git a/web/src/components/nameInputModal.tsx b/web/src/components/nameInputModal.tsx new file mode 100644 index 0000000..2343b74 --- /dev/null +++ b/web/src/components/nameInputModal.tsx @@ -0,0 +1,82 @@ +import React, { useState } from 'react' + +import { Button, DialogActions, DialogContent, TextField } from '@mui/material' + +import { BootstrapDialogTitle } from './dialogTitle' +import { BootstrapDialog } from './modal' + +type NameInputModalProps = { + open: boolean + setOpen: React.Dispatch> + isFolder: boolean + add: (name: string) => void +} + +const NameInputModal = ({ + open, + setOpen, + isFolder, + add +}: NameInputModalProps) => { + const [name, setName] = useState('') + const [hasError, setHasError] = useState(false) + const [errorText, setErrorText] = useState('') + + const handleChange = (event: React.ChangeEvent) => { + const value = event.target.value + + const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/ + const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/ + const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i + + const specialChars = isFolder ? folderNameRegex : fileNameRegex + + if (specialChars.test(value)) { + setHasError(true) + setErrorText('can not have special characters') + } else if (!isFolder && fileNameExtensionRegex.test(value)) { + setHasError(true) + setErrorText('can not add file with extensions [exe, sh, htaccess]') + } else { + setHasError(false) + setErrorText('') + } + + setName(value) + } + + return ( + setOpen(false)} open={open}> + + {isFolder ? 'Add Folder' : 'Add File'} + + + + + + + + + + ) +} + +export default NameInputModal diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx index 9b7db0f..b244b05 100644 --- a/web/src/components/tree.tsx +++ b/web/src/components/tree.tsx @@ -4,6 +4,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import ChevronRightIcon from '@mui/icons-material/ChevronRight' import DeleteConfirmationModal from './deleteConfirmationModal' +import NameInputModal from './nameInputModal' import { TreeNode } from '../utils/types' @@ -12,6 +13,8 @@ type Props = { selectedFilePath: string handleSelect: (filePath: string) => void deleteNode: (path: string, isFolder: boolean) => void + addFile: (path: string) => void + addFolder: (path: string) => void defaultExpanded?: string[] } @@ -20,6 +23,8 @@ const TreeView = ({ selectedFilePath, handleSelect, deleteNode, + addFile, + addFolder, defaultExpanded }: Props) => { return ( @@ -35,6 +40,8 @@ const TreeView = ({ selectedFilePath={selectedFilePath} handleSelect={handleSelect} deleteNode={deleteNode} + addFile={addFile} + addFolder={addFolder} defaultExpanded={defaultExpanded} /> @@ -48,12 +55,16 @@ const TreeViewNode = ({ selectedFilePath, handleSelect, deleteNode, + addFile, + addFolder, defaultExpanded }: Props) => { const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false) const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] = useState('') + const [nameInputModalOpen, setNameInputModalOpen] = useState(false) + const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false) const [childVisible, setChildVisibility] = useState(false) const [contextMenu, setContextMenu] = useState<{ mouseX: number @@ -108,6 +119,25 @@ const TreeViewNode = ({ deleteNode(node.relativePath, node.isFolder) } + const handleNewFolderItemClick = () => { + setContextMenu(null) + setNameInputModalOpen(true) + setNameInputModalForFolder(true) + } + + const handleNewFileItemClick = () => { + setContextMenu(null) + setNameInputModalOpen(true) + setNameInputModalForFolder(false) + } + + const addFileFolder = (name: string) => { + setNameInputModalOpen(false) + const path = node.relativePath + '/' + name + if (nameInputModalForFolder) addFolder(path) + else addFile(path) + } + return (
  • @@ -131,6 +161,8 @@ const TreeViewNode = ({ selectedFilePath={selectedFilePath} handleSelect={handleSelect} deleteNode={deleteNode} + addFile={addFile} + addFolder={addFolder} defaultExpanded={defaultExpanded} /> ))} @@ -141,6 +173,12 @@ const TreeViewNode = ({ message={deleteConfirmationModalMessage} _delete={deleteConfirm} /> + setContextMenu(null)} @@ -153,7 +191,16 @@ const TreeViewNode = ({ > {node.isFolder && ['Add Folder', 'Add File'].map((item) => ( - {item} + + item === 'Add Folder' + ? handleNewFolderItemClick() + : handleNewFileItemClick() + } + > + {item} + ))} Rename diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index 1dd7051..c8f9751 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -88,6 +88,7 @@ const Studio = () => { directoryData={directoryData} handleSelect={handleSelect} removeFileFromTree={removeFileFromTree} + refreshSideBar={fetchDirectoryData} /> void removeFileFromTree: (filePath: string) => void + refreshSideBar: () => void } const SideBar = ({ selectedFilePath, directoryData, handleSelect, - removeFileFromTree + removeFileFromTree, + refreshSideBar }: Props) => { + const [isLoading, setIsLoading] = useState(false) + const [openModal, setOpenModal] = useState(false) + const [modalTitle, setModalTitle] = useState('') + const [modalPayload, setModalPayload] = useState('') + const [openSnackbar, setOpenSnackbar] = useState(false) + const [snackbarMessage, setSnackbarMessage] = useState('') + const [snackbarSeverity, setSnackbarSeverity] = useState( + AlertSeverityType.Success + ) const defaultExpanded = useMemo(() => { const splittedPath = selectedFilePath.split('/') const arr = [''] @@ -34,6 +47,7 @@ const SideBar = ({ }, [selectedFilePath]) const deleteNode = (path: string, isFolder: boolean) => { + setIsLoading(true) const axiosPromise = axios.delete( `/SASjsApi/drive/${ isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}` @@ -41,10 +55,71 @@ const SideBar = ({ ) axiosPromise - .then(() => removeFileFromTree(path)) - .catch((err) => { - console.log(err) + .then(() => { + removeFileFromTree(path) + setSnackbarMessage('Deleted!') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) + setOpenModal(true) + }) + .finally(() => setIsLoading(false)) + } + + const addFile = (filePath: string) => { + const formData = new FormData() + const stringBlob = new Blob([''], { type: 'text/plain' }) + formData.append('file', stringBlob) + formData.append('filePath', filePath) + + setIsLoading(true) + axios + .post('/SASjsApi/drive/file', formData) + .then(() => { + setSnackbarMessage('File added!') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) + refreshSideBar() + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) + setOpenModal(true) + }) + .finally(() => setIsLoading(false)) + } + + const addFolder = (folderPath: string) => { + setIsLoading(true) + axios + .post('/SASjsApi/drive/folder', { folderPath }) + .then(() => { + setSnackbarMessage('Folder added!') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) + refreshSideBar() + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) + setOpenModal(true) + }) + .finally(() => setIsLoading(false)) } return ( @@ -56,6 +131,12 @@ const SideBar = ({ [`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' } }} > + theme.zIndex.drawer + 1 }} + open={isLoading} + > + + {directoryData && ( @@ -64,10 +145,24 @@ const SideBar = ({ selectedFilePath={selectedFilePath} handleSelect={handleSelect} deleteNode={deleteNode} + addFile={addFile} + addFolder={addFolder} defaultExpanded={defaultExpanded} /> )} + + ) } From 48688a65473953d8f4ebd71baaecb5500130105c Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 20 Jul 2022 16:52:49 +0500 Subject: [PATCH 14/29] chore: update swagger docs --- api/public/swagger.yaml | 30 +++++++++++++++--------------- api/tsoa.json | 36 ++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index b42c4b4..5d3e6af 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -1782,35 +1782,35 @@ servers: url: / tags: - - name: Info - description: 'Get Server Information' - - - name: Session - description: 'Get Session information' - - - name: User - description: 'Operations with users' - - - name: Permission - description: 'Operations about permissions' + name: Auth + description: 'Operations about auth' - name: Client description: 'Operations about clients' - - name: Auth - description: 'Operations about auth' + name: CODE + description: 'Execution of code (various runtimes are supported)' - name: Drive description: 'Operations on SASjs Drive' - name: Group description: 'Operations on groups and group memberships' + - + name: Info + description: 'Get Server Information' + - + name: Permission + description: 'Operations about permissions' + - + name: Session + description: 'Get Session information' - name: STP description: 'Execution of Stored Programs' - - name: CODE - description: 'Execution of code (various runtimes are supported)' + name: User + description: 'Operations with users' - name: Web description: 'Operations on Web' diff --git a/api/tsoa.json b/api/tsoa.json index 6643337..a3a0c86 100644 --- a/api/tsoa.json +++ b/api/tsoa.json @@ -12,28 +12,16 @@ }, "tags": [ { - "name": "Info", - "description": "Get Server Information" - }, - { - "name": "Session", - "description": "Get Session information" - }, - { - "name": "User", - "description": "Operations with users" - }, - { - "name": "Permission", - "description": "Operations about permissions" + "name": "Auth", + "description": "Operations about auth" }, { "name": "Client", "description": "Operations about clients" }, { - "name": "Auth", - "description": "Operations about auth" + "name": "CODE", + "description": "Execution of code (various runtimes are supported)" }, { "name": "Drive", @@ -43,13 +31,25 @@ "name": "Group", "description": "Operations on groups and group memberships" }, + { + "name": "Info", + "description": "Get Server Information" + }, + { + "name": "Permission", + "description": "Operations about permissions" + }, + { + "name": "Session", + "description": "Get Session information" + }, { "name": "STP", "description": "Execution of Stored Programs" }, { - "name": "CODE", - "description": "Execution of code (various runtimes are supported)" + "name": "User", + "description": "Operations with users" }, { "name": "Web", From fdcaba9d56cddea5d56d7de5a172f1bb49be3db5 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 20 Jul 2022 23:45:11 +0500 Subject: [PATCH 15/29] feat: implemented api for renaming file/folder --- api/public/swagger.yaml | 50 +++++++++++++++++++++++++ api/src/controllers/drive.ts | 71 ++++++++++++++++++++++++++++++++++++ api/src/routes/api/drive.ts | 16 +++++++- api/src/utils/validation.ts | 10 ++++- 4 files changed, 144 insertions(+), 3 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 5d3e6af..90af057 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -250,6 +250,21 @@ components: - folderPath type: object additionalProperties: false + RenamePayload: + properties: + oldPath: + type: string + description: 'Old path of file/folder' + example: /Public/someFolder + newPath: + type: string + description: 'New path of file/folder' + example: /Public/newFolder + required: + - oldPath + - newPath + type: object + additionalProperties: false TreeNode: properties: name: @@ -1034,6 +1049,41 @@ paths: application/json: schema: $ref: '#/components/schemas/AddFolderPayload' + /SASjsApi/drive/rename: + post: + operationId: Rename + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/FileFolderResponse' + examples: + 'Example 1': + value: {status: success} + '409': + description: 'Folder already exists' + content: + application/json: + schema: + $ref: '#/components/schemas/FileFolderResponse' + examples: + 'Example 1': + value: {status: failure, message: 'rename request failed.'} + summary: 'Renames a file/folder in SASjs Drive' + tags: + - Drive + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RenamePayload' /SASjsApi/drive/filetree: get: operationId: GetFileTree diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index d99a4d2..41acb31 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -72,6 +72,19 @@ interface AddFolderPayload { folderPath: string } +interface RenamePayload { + /** + * Old path of file/folder + * @example "/Public/someFolder" + */ + oldPath: string + /** + * New path of file/folder + * @example "/Public/newFolder" + */ + newPath: string +} + const fileTreeExample = getTreeExample() const successDeployResponse: DeployResponse = { @@ -241,6 +254,24 @@ export class DriveController { return updateFile((_filePath ?? filePath)!, file) } + /** + * @summary Renames a file/folder in SASjs Drive + * + */ + @Example({ + status: 'success' + }) + @Response(409, 'Folder already exists', { + status: 'failure', + message: 'rename request failed.' + }) + @Post('/rename') + public async rename( + @Body() body: RenamePayload + ): Promise { + return rename(body.oldPath, body.newPath) + } + /** * @summary Fetch file tree within SASjs Drive. * @@ -418,6 +449,46 @@ const addFolder = async (folderPath: string): Promise => { return { status: 'success' } } +const rename = async ( + oldPath: string, + newPath: string +): Promise => { + const drivePath = getFilesFolder() + + const oldPathFull = path + .join(drivePath, oldPath) + .replace(new RegExp('/', 'g'), path.sep) + + const newPathFull = path + .join(drivePath, newPath) + .replace(new RegExp('/', 'g'), path.sep) + + if (!oldPathFull.includes(drivePath)) { + throw new Error('Old path is outside drive.') + } + + if (!newPathFull.includes(drivePath)) { + throw new Error('New path is outside drive.') + } + + if (await folderExists(oldPathFull)) { + if (await folderExists(newPathFull)) { + throw new Error('Folder already exists.') + } else moveFile(oldPathFull, newPathFull) + + return { status: 'success' } + } + + if (await fileExists(oldPathFull)) { + if (await fileExists(newPathFull)) { + throw new Error('File already exists.') + } else moveFile(oldPath, newPathFull) + return { status: 'success' } + } + + throw new Error('No file/folder found for provided path.') +} + const updateFile = async ( filePath: string, multerFile: Express.Multer.File diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 90fc707..1af8f3e 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -13,7 +13,8 @@ import { fileParamValidation, folderBodyValidation, folderParamValidation, - isZipFile + isZipFile, + renameBodyValidation } from '../../utils' const controller = new DriveController() @@ -232,6 +233,19 @@ driveRouter.patch( } ) +driveRouter.post('/rename', async (req, res) => { + const { error, value: body } = renameBodyValidation(req.body) + + if (error) return res.status(400).send(error.details[0].message) + + try { + const response = await controller.rename(body) + res.send(response) + } catch (err: any) { + res.status(403).send(err.toString()) + } +}) + driveRouter.get('/fileTree', async (req, res) => { try { const response = await controller.getFileTree() diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 0dae10c..307e67a 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -140,12 +140,18 @@ export const fileParamValidation = (data: any): Joi.ValidationResult => export const folderParamValidation = (data: any): Joi.ValidationResult => Joi.object({ - _folderPath: Joi.string() + _folderPath: Joi.string().required() }).validate(data) export const folderBodyValidation = (data: any): Joi.ValidationResult => Joi.object({ - folderPath: Joi.string() + folderPath: Joi.string().required() + }).validate(data) + +export const renameBodyValidation = (data: any): Joi.ValidationResult => + Joi.object({ + oldPath: Joi.string().required(), + newPath: Joi.string().required() }).validate(data) export const runCodeValidation = (data: any): Joi.ValidationResult => From 7010a6a1201720d0eb4093267a344fb828b90a2f Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 20 Jul 2022 23:46:39 +0500 Subject: [PATCH 16/29] feat: implemented the functionality for renaming file/folder from context menu --- web/src/components/nameInputModal.tsx | 14 ++++++---- web/src/components/tree.tsx | 38 +++++++++++++++++++++++++-- web/src/containers/Studio/sideBar.tsx | 26 ++++++++++++++++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/web/src/components/nameInputModal.tsx b/web/src/components/nameInputModal.tsx index 2343b74..32e6f52 100644 --- a/web/src/components/nameInputModal.tsx +++ b/web/src/components/nameInputModal.tsx @@ -8,15 +8,19 @@ import { BootstrapDialog } from './modal' type NameInputModalProps = { open: boolean setOpen: React.Dispatch> + title: string isFolder: boolean - add: (name: string) => void + actionLabel: string + action: (name: string) => void } const NameInputModal = ({ open, setOpen, + title, isFolder, - add + actionLabel, + action }: NameInputModalProps) => { const [name, setName] = useState('') const [hasError, setHasError] = useState(false) @@ -48,7 +52,7 @@ const NameInputModal = ({ return ( setOpen(false)} open={open}> - {isFolder ? 'Add Folder' : 'Add File'} + {title} { - add(name) + action(name) }} disabled={hasError || !name} > - Add + {actionLabel} diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx index b244b05..8394efe 100644 --- a/web/src/components/tree.tsx +++ b/web/src/components/tree.tsx @@ -15,6 +15,7 @@ type Props = { deleteNode: (path: string, isFolder: boolean) => void addFile: (path: string) => void addFolder: (path: string) => void + rename: (oldPath: string, newPath: string) => void defaultExpanded?: string[] } @@ -25,6 +26,7 @@ const TreeView = ({ deleteNode, addFile, addFolder, + rename, defaultExpanded }: Props) => { return ( @@ -42,6 +44,7 @@ const TreeView = ({ deleteNode={deleteNode} addFile={addFile} addFolder={addFolder} + rename={rename} defaultExpanded={defaultExpanded} /> @@ -57,6 +60,7 @@ const TreeViewNode = ({ deleteNode, addFile, addFolder, + rename, defaultExpanded }: Props) => { const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = @@ -64,6 +68,8 @@ const TreeViewNode = ({ const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] = useState('') const [nameInputModalOpen, setNameInputModalOpen] = useState(false) + const [nameInputModalTitle, setNameInputModalTitle] = useState('') + const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('') const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false) const [childVisible, setChildVisibility] = useState(false) const [contextMenu, setContextMenu] = useState<{ @@ -122,12 +128,16 @@ const TreeViewNode = ({ const handleNewFolderItemClick = () => { setContextMenu(null) setNameInputModalOpen(true) + setNameInputModalTitle('Add Folder') + setNameInputModalActionLabel('Add') setNameInputModalForFolder(true) } const handleNewFileItemClick = () => { setContextMenu(null) setNameInputModalOpen(true) + setNameInputModalTitle('Add File') + setNameInputModalActionLabel('Add') setNameInputModalForFolder(false) } @@ -138,6 +148,23 @@ const TreeViewNode = ({ else addFile(path) } + const handleRenameItemClick = () => { + setContextMenu(null) + setNameInputModalOpen(true) + setNameInputModalTitle('Rename') + setNameInputModalActionLabel('Rename') + setNameInputModalForFolder(node.isFolder) + } + + const renameFileFolder = (name: string) => { + setNameInputModalOpen(false) + const oldPath = node.relativePath + const splittedPath = node.relativePath.split('/') + splittedPath.splice(-1, 1, name) + const newPath = splittedPath.join('/') + rename(oldPath, newPath) + } + return (
  • @@ -163,6 +190,7 @@ const TreeViewNode = ({ deleteNode={deleteNode} addFile={addFile} addFolder={addFolder} + rename={rename} defaultExpanded={defaultExpanded} /> ))} @@ -176,8 +204,12 @@ const TreeViewNode = ({ ))} - Rename + + Rename + Delete diff --git a/web/src/containers/Studio/sideBar.tsx b/web/src/containers/Studio/sideBar.tsx index 6b177d8..dc9f767 100644 --- a/web/src/containers/Studio/sideBar.tsx +++ b/web/src/containers/Studio/sideBar.tsx @@ -122,6 +122,31 @@ const SideBar = ({ .finally(() => setIsLoading(false)) } + const rename = (oldPath: string, newPath: string) => { + setIsLoading(true) + axios + .post('/SASjsApi/drive/rename', { oldPath, newPath }) + .then(() => { + setSnackbarMessage('Successfully Renamed') + setSnackbarSeverity(AlertSeverityType.Success) + setOpenSnackbar(true) + if (oldPath === selectedFilePath) handleSelect(newPath) + else if (selectedFilePath.startsWith(oldPath)) + handleSelect(selectedFilePath.replace(oldPath, newPath)) + refreshSideBar() + }) + .catch((err) => { + setModalTitle('Abort') + setModalPayload( + typeof err.response.data === 'object' + ? JSON.stringify(err.response.data) + : err.response.data + ) + setOpenModal(true) + }) + .finally(() => setIsLoading(false)) + } + return ( )} From 06d7c91fc34620a954df1fd1c682eff370f79ca6 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Wed, 20 Jul 2022 23:53:42 +0500 Subject: [PATCH 17/29] fix: remove drive component --- web/src/App.tsx | 2 - web/src/components/header.tsx | 6 - web/src/containers/Drive/index.tsx | 106 ---------------- web/src/containers/Drive/main.tsx | 173 --------------------------- web/src/containers/Drive/sideBar.tsx | 100 ---------------- 5 files changed, 387 deletions(-) delete mode 100644 web/src/containers/Drive/index.tsx delete mode 100644 web/src/containers/Drive/main.tsx delete mode 100644 web/src/containers/Drive/sideBar.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 4ac2351..39c11cf 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -6,7 +6,6 @@ import { theme } from './theme' import Login from './components/login' import Header from './components/header' import Home from './components/home' -import Drive from './containers/Drive' import Studio from './containers/Studio' import Settings from './containers/Settings' @@ -36,7 +35,6 @@ function App() {
    } /> - } /> } /> } /> } /> diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index d80d01a..96c6bc0 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -83,12 +83,6 @@ const Header = (props: any) => { onChange={handleTabChange} > - -} - -const Drive = () => { - const location = useLocation() - const baseUrl = window.location.origin - - const [selectedFilePath, setSelectedFilePath] = useState('') - const [directoryData, setDirectoryData] = useState(null) - - const setFilePathOnMount = useCallback(() => { - const queryParams = new URLSearchParams(location.search) - setSelectedFilePath(queryParams.get('filePath') ?? '') - }, [location.search]) - - useEffect(() => { - axios - .get(`/SASjsApi/drive/fileTree`) - .then((res: any) => { - if (res.data && res.data?.status === 'success') { - setDirectoryData(res.data.tree) - } - }) - .catch((err) => { - console.log(err) - }) - setFilePathOnMount() - }, [setFilePathOnMount]) - - const handleSelect = (node: TreeNode) => { - if (node.children.length) return - - if (!node.name.includes('.')) return - - window.history.pushState( - '', - '', - `${baseUrl}/#/SASjsDrive?filePath=${node.relativePath}` - ) - setSelectedFilePath(node.relativePath) - } - - const removeFileFromTree = (path: string) => { - if (directoryData) { - const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode - findAndRemoveNode(newTree, newTree, path) - setDirectoryData(newTree) - } - } - - const findAndRemoveNode = ( - node: TreeNode, - parentNode: TreeNode, - path: string - ) => { - if (node.relativePath === path) { - removeNodeFromParent(parentNode, path) - return true - } - if (Array.isArray(node.children)) { - for (let i = 0; i < node.children.length; i++) { - if (findAndRemoveNode(node.children[i], node, path)) return - } - } - } - - const removeNodeFromParent = (parent: TreeNode, path: string) => { - const index = parent.children.findIndex( - (node) => node.relativePath === path - ) - if (index !== -1) { - parent.children.splice(index, 1) - } - } - - return ( - - - -
    - - ) -} - -export default Drive diff --git a/web/src/containers/Drive/main.tsx b/web/src/containers/Drive/main.tsx deleted file mode 100644 index e3b48c5..0000000 --- a/web/src/containers/Drive/main.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { Link } from 'react-router-dom' -import axios from 'axios' - -import Editor from 'react-monaco-editor' - -import Box from '@mui/material/Box' -import Paper from '@mui/material/Paper' -import Stack from '@mui/material/Stack' -import Button from '@mui/material/Button' -import Toolbar from '@mui/material/Toolbar' -import CircularProgress from '@mui/material/CircularProgress' - -type Props = { - selectedFilePath: string - removeFileFromTree: (path: string) => void -} - -const Main = (props: Props) => { - const baseUrl = window.location.origin - - const [isLoading, setIsLoading] = useState(false) - const [fileContentBeforeEdit, setFileContentBeforeEdit] = useState('') - const [fileContent, setFileContent] = useState('') - const [editMode, setEditMode] = useState(false) - - useEffect(() => { - if (props.selectedFilePath) { - setIsLoading(true) - axios - .get(`/SASjsApi/drive/file?_filePath=${props.selectedFilePath}`) - .then((res: any) => { - setFileContent(res.data) - }) - .catch((err) => { - console.log(err) - }) - .finally(() => { - setIsLoading(false) - }) - } - }, [props.selectedFilePath]) - - const handleDeleteBtnClick = () => { - setIsLoading(true) - - const filePath = props.selectedFilePath - - axios - .delete(`/SASjsApi/drive/file?_filePath=${filePath}`) - .then((res) => { - setFileContent('') - props.removeFileFromTree(filePath) - window.history.pushState('', '', `${baseUrl}/#/SASjsDrive`) - }) - .catch((err) => { - console.log(err) - }) - .finally(() => { - setIsLoading(false) - }) - } - - const handleEditSaveBtnClick = () => { - if (!editMode) { - setFileContentBeforeEdit(fileContent) - setEditMode(true) - } else { - setIsLoading(true) - - const formData = new FormData() - - const stringBlob = new Blob([fileContent], { type: 'text/plain' }) - formData.append('file', stringBlob, 'filename.sas') - formData.append('filePath', props.selectedFilePath) - - axios - .patch(`/SASjsApi/drive/file`, formData) - .then((res) => { - setEditMode(false) - }) - .catch((err) => { - console.log(err) - }) - .finally(() => { - setIsLoading(false) - }) - } - } - - const handleCancelExecuteBtnClick = () => { - if (editMode) { - setFileContent(fileContentBeforeEdit) - setEditMode(false) - } else { - window.open( - `${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath}` - ) - } - } - - return ( - - - - {isLoading && ( - - )} - {!isLoading && props?.selectedFilePath && !editMode && ( - {fileContent} - )} - {!isLoading && props?.selectedFilePath && editMode && ( - { - if (val) setFileContent(val) - }} - /> - )} - - - - - - {props?.selectedFilePath && ( - - )} - - - ) -} - -export default Main diff --git a/web/src/containers/Drive/sideBar.tsx b/web/src/containers/Drive/sideBar.tsx deleted file mode 100644 index b0aab5b..0000000 --- a/web/src/containers/Drive/sideBar.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useMemo } from 'react' - -import { makeStyles } from '@mui/styles' - -import Box from '@mui/material/Box' -import Drawer from '@mui/material/Drawer' -import Toolbar from '@mui/material/Toolbar' -import ListItem from '@mui/material/ListItem' -import ListItemText from '@mui/material/ListItemText' - -import TreeView from '@mui/lab/TreeView' -import TreeItem from '@mui/lab/TreeItem' - -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import ChevronRightIcon from '@mui/icons-material/ChevronRight' - -import { TreeNode } from '.' - -const useStyles = makeStyles(() => ({ - root: { - '& .MuiTreeItem-content': { - width: 'auto' - } - }, - listItem: { - padding: 0 - } -})) - -const drawerWidth = 240 - -type Props = { - selectedFilePath: string - directoryData: TreeNode | null - handleSelect: (node: TreeNode) => void -} - -const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => { - const classes = useStyles() - - const defaultExpanded = useMemo(() => { - const splittedPath = selectedFilePath.split('/') - const arr = [''] - let nodeId = '' - splittedPath.forEach((path) => { - if (path !== '') { - nodeId += '/' + path - arr.push(nodeId) - } - }) - return arr - }, [selectedFilePath]) - - const renderTree = (nodes: TreeNode) => ( - handleSelect(nodes)} - > - - - } - > - {Array.isArray(nodes.children) - ? nodes.children.map((node) => renderTree(node)) - : null} - - ) - - return ( - - - - {directoryData && ( - } - defaultExpandIcon={} - defaultExpanded={defaultExpanded} - selected={defaultExpanded.slice(-1)} - > - {renderTree(directoryData)} - - )} - - - ) -} - -export default SideBar From efcefd2a42c8c383aeabc67641670b365f8aee6c Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 21 Jul 2022 13:25:46 +0500 Subject: [PATCH 18/29] chore: quick fix --- web/src/containers/Studio/editor.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx index a3b0be0..f84495b 100644 --- a/web/src/containers/Studio/editor.tsx +++ b/web/src/containers/Studio/editor.tsx @@ -215,11 +215,7 @@ const SASjsEditor = ({ } return ( - + theme.zIndex.drawer + 1 }} open={isLoading} @@ -298,6 +294,8 @@ const SASjsEditor = ({ /> Date: Thu, 21 Jul 2022 14:08:44 +0500 Subject: [PATCH 19/29] chore: modified folderParamValidation method --- api/src/routes/api/drive.ts | 2 +- api/src/utils/validation.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index 1af8f3e..c1e94c0 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -152,7 +152,7 @@ driveRouter.delete('/file', async (req, res) => { }) driveRouter.delete('/folder', async (req, res) => { - const { error: errQ, value: query } = folderParamValidation(req.query) + const { error: errQ, value: query } = folderParamValidation(req.query, true) if (errQ) return res.status(400).send(errQ.details[0].message) diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 307e67a..09868c7 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -138,9 +138,12 @@ export const fileParamValidation = (data: any): Joi.ValidationResult => _filePath: filePathSchema }).validate(data) -export const folderParamValidation = (data: any): Joi.ValidationResult => +export const folderParamValidation = ( + data: any, + folderPathRequired?: boolean +): Joi.ValidationResult => Joi.object({ - _folderPath: Joi.string().required() + _folderPath: folderPathRequired ? Joi.string().required() : Joi.string() }).validate(data) export const folderBodyValidation = (data: any): Joi.ValidationResult => From 03cb89d14f345f8dacb16243d7750904fc09c6ce Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 21 Jul 2022 23:03:40 +0500 Subject: [PATCH 20/29] chore: code fixes --- api/src/controllers/drive.ts | 4 ++-- api/src/utils/getAuthorizedRoutes.ts | 1 + web/src/components/tree.tsx | 18 ++++++++---------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index 41acb31..36d2127 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -395,7 +395,7 @@ const deleteFolder = async (folderPath: string) => { throw new Error('Cannot delete file outside drive.') } - if (!(await fileExists(folderPathFull))) { + if (!(await folderExists(folderPathFull))) { throw new Error('Folder does not exist.') } @@ -482,7 +482,7 @@ const rename = async ( if (await fileExists(oldPathFull)) { if (await fileExists(newPathFull)) { throw new Error('File already exists.') - } else moveFile(oldPath, newPathFull) + } else moveFile(oldPathFull, newPathFull) return { status: 'success' } } diff --git a/api/src/utils/getAuthorizedRoutes.ts b/api/src/utils/getAuthorizedRoutes.ts index 93412fa..82f581b 100644 --- a/api/src/utils/getAuthorizedRoutes.ts +++ b/api/src/utils/getAuthorizedRoutes.ts @@ -9,6 +9,7 @@ const StaticAuthorizedRoutes = [ '/SASjsApi/drive/file', '/SASjsApi/drive/folder', '/SASjsApi/drive/fileTree', + '/SASjsApi/drive/rename', '/SASjsApi/permission' ] diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx index 8394efe..135a876 100644 --- a/web/src/components/tree.tsx +++ b/web/src/components/tree.tsx @@ -221,19 +221,17 @@ const TreeViewNode = ({ : undefined } > - {node.isFolder && - ['Add Folder', 'Add File'].map((item) => ( + {node.isFolder && ( +
    + Add Folder - item === 'Add Folder' - ? handleNewFolderItemClick() - : handleNewFileItemClick() - } + disabled={!node.relativePath} + onClick={handleNewFileItemClick} > - {item} + Add File - ))} +
    + )} Rename From cc1e4543fc5b23d9315ccd398f01343127adf49b Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 21 Jul 2022 23:03:56 +0500 Subject: [PATCH 21/29] chore: add specs --- api/src/routes/api/spec/drive.spec.ts | 191 ++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts index 3bf2109..5651ec3 100644 --- a/api/src/routes/api/spec/drive.spec.ts +++ b/api/src/routes/api/spec/drive.spec.ts @@ -89,6 +89,12 @@ describe('drive', () => { principalId: dbUser.id, setting: PermissionSetting.grant }) + await permissionController.createPermission({ + uri: '/SASjsApi/drive/rename', + principalType: PrincipalType.user, + principalId: dbUser.id, + setting: PermissionSetting.grant + }) }) afterAll(async () => { @@ -582,6 +588,84 @@ describe('drive', () => { expect(res.body).toEqual({}) }) }) + + describe('post', () => { + const folderApi = '/SASjsApi/drive/folder' + const pathToDrive = fileUtilModules.getFilesFolder() + + afterEach(async () => { + await deleteFolder(path.join(pathToDrive, 'post')) + }) + + it('should create a folder on drive', async () => { + const res = await request(app) + .post(folderApi) + .auth(accessToken, { type: 'bearer' }) + .send({ folderPath: '/post/folder' }) + + expect(res.statusCode).toEqual(200) + expect(res.body).toEqual({ + status: 'success' + }) + }) + + it('should respond with Forbidden if the folder already exists', async () => { + await createFolder(path.join(pathToDrive, '/post/folder')) + + const res = await request(app) + .post(folderApi) + .auth(accessToken, { type: 'bearer' }) + .send({ folderPath: '/post/folder' }) + + expect(res.statusCode).toEqual(403) + }) + + it('should respond with Forbidden if the folderPath is outside drive', async () => { + const res = await request(app) + .post(folderApi) + .auth(accessToken, { type: 'bearer' }) + .send({ folderPath: '../sample' }) + + expect(res.statusCode).toEqual(403) + }) + }) + + describe('delete', () => { + const folderApi = '/SASjsApi/drive/folder' + const pathToDrive = fileUtilModules.getFilesFolder() + + it('should delete a folder on drive', async () => { + await createFolder(path.join(pathToDrive, 'delete')) + + const res = await request(app) + .delete(folderApi) + .auth(accessToken, { type: 'bearer' }) + .query({ _folderPath: 'delete' }) + + expect(res.statusCode).toEqual(200) + expect(res.body).toEqual({ + status: 'success' + }) + }) + + it('should respond with Forbidden if the folder does not exists', async () => { + const res = await request(app) + .delete(folderApi) + .auth(accessToken, { type: 'bearer' }) + .query({ _folderPath: 'notExists' }) + + expect(res.statusCode).toEqual(403) + }) + + it('should respond with Forbidden if the folderPath is outside drive', async () => { + const res = await request(app) + .delete(folderApi) + .auth(accessToken, { type: 'bearer' }) + .query({ _folderPath: '../outsideDrive' }) + + expect(res.statusCode).toEqual(403) + }) + }) }) describe('file', () => { @@ -966,6 +1050,113 @@ describe('drive', () => { }) }) }) + + describe('rename', () => { + const renameApi = '/SASjsApi/drive/rename' + const pathToDrive = fileUtilModules.getFilesFolder() + + afterEach(async () => { + await deleteFolder(path.join(pathToDrive, 'rename')) + }) + + it('should rename a folder', async () => { + await createFolder(path.join(pathToDrive, 'rename', 'folder')) + + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: '/rename/folder', newPath: '/rename/renamed' }) + + expect(res.statusCode).toEqual(200) + expect(res.body).toEqual({ + status: 'success' + }) + }) + + it('should rename a file', async () => { + await createFile( + path.join(pathToDrive, 'rename', 'file.txt'), + 'some file content' + ) + + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ + oldPath: '/rename/file.txt', + newPath: '/rename/renamed.txt' + }) + + expect(res.statusCode).toEqual(200) + expect(res.body).toEqual({ + status: 'success' + }) + }) + + it('should respond with forbidden if the oldPath is outside drive', async () => { + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: '../outside', newPath: 'renamed' }) + + expect(res.statusCode).toEqual(403) + }) + + it('should respond with forbidden if the newPath is outside drive', async () => { + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: 'older', newPath: '../outside' }) + + expect(res.statusCode).toEqual(403) + }) + + it('should respond with forbidden if the folder does not exist', async () => { + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' }) + + expect(res.statusCode).toEqual(403) + }) + + it('should respond with forbidden if the folder already exists', async () => { + await createFolder(path.join(pathToDrive, 'rename', 'folder')) + await createFolder(path.join(pathToDrive, 'rename', 'exists')) + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: '/rename/folder', newPath: '/rename/exists' }) + + expect(res.statusCode).toEqual(403) + }) + + it('should respond with forbidden if the file does not exist', async () => { + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' }) + + expect(res.statusCode).toEqual(403) + }) + + it('should respond with forbidden if the file already exists', async () => { + await createFile( + path.join(pathToDrive, 'rename', 'file.txt'), + 'some file content' + ) + await createFile( + path.join(pathToDrive, 'rename', 'exists.txt'), + 'some existing content' + ) + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' }) + + expect(res.statusCode).toEqual(403) + }) + }) }) const getExampleService = (): ServiceMember => From 57daad0c264c7132b1dd5ca635df11bd08939ec3 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 22 Jul 2022 16:58:26 +0500 Subject: [PATCH 22/29] chore: error response codes for drive api --- api/src/controllers/drive.ts | 185 +++++++++++++++++--------- api/src/routes/api/drive.ts | 50 +++++-- api/src/routes/api/spec/drive.spec.ts | 106 ++++++++------- 3 files changed, 224 insertions(+), 117 deletions(-) diff --git a/api/src/controllers/drive.ts b/api/src/controllers/drive.ts index 36d2127..1b7a6ca 100644 --- a/api/src/controllers/drive.ts +++ b/api/src/controllers/drive.ts @@ -318,13 +318,19 @@ const getFile = async (req: express.Request, filePath: string) => { .join(getFilesFolder(), filePath) .replace(new RegExp('/', 'g'), path.sep) - if (!filePathFull.includes(driveFilesPath)) { - throw new Error('Cannot get file outside drive.') - } + if (!filePathFull.includes(driveFilesPath)) + throw { + code: 400, + status: 'Bad Request', + message: `Can't get file outside drive.` + } - if (!(await fileExists(filePathFull))) { - throw new Error("File doesn't exist.") - } + if (!(await fileExists(filePathFull))) + throw { + code: 404, + status: 'Not Found', + message: `File doesn't exist.` + } const extension = path.extname(filePathFull).toLowerCase() if (extension === '.sas') { @@ -342,17 +348,26 @@ const getFolder = async (folderPath?: string) => { .join(getFilesFolder(), folderPath) .replace(new RegExp('/', 'g'), path.sep) - if (!folderPathFull.includes(driveFilesPath)) { - throw new Error('Cannot get folder outside drive.') - } + if (!folderPathFull.includes(driveFilesPath)) + throw { + code: 400, + status: 'Bad Request', + message: `Can't get folder outside drive.` + } - if (!(await folderExists(folderPathFull))) { - throw new Error("Folder doesn't exist.") - } + if (!(await folderExists(folderPathFull))) + throw { + code: 404, + status: 'Not Found', + message: `Folder doesn't exist.` + } - if (!(await isFolder(folderPathFull))) { - throw new Error('Not a Folder.') - } + if (!(await isFolder(folderPathFull))) + throw { + code: 400, + status: 'Bad Request', + message: 'Not a Folder.' + } const files: string[] = await listFilesInFolder(folderPathFull) const folders: string[] = await listSubFoldersInFolder(folderPathFull) @@ -371,13 +386,19 @@ const deleteFile = async (filePath: string) => { .join(getFilesFolder(), filePath) .replace(new RegExp('/', 'g'), path.sep) - if (!filePathFull.includes(driveFilesPath)) { - throw new Error('Cannot delete file outside drive.') - } + if (!filePathFull.includes(driveFilesPath)) + throw { + code: 400, + status: 'Bad Request', + message: `Can't delete file outside drive.` + } - if (!(await fileExists(filePathFull))) { - throw new Error('File does not exist.') - } + if (!(await fileExists(filePathFull))) + throw { + code: 404, + status: 'Not Found', + message: `File doesn't exist.` + } await deleteFileOnSystem(filePathFull) @@ -391,13 +412,19 @@ const deleteFolder = async (folderPath: string) => { .join(getFilesFolder(), folderPath) .replace(new RegExp('/', 'g'), path.sep) - if (!folderPathFull.includes(driveFolderPath)) { - throw new Error('Cannot delete file outside drive.') - } + if (!folderPathFull.includes(driveFolderPath)) + throw { + code: 400, + status: 'Bad Request', + message: `Can't delete folder outside drive.` + } - if (!(await folderExists(folderPathFull))) { - throw new Error('Folder does not exist.') - } + if (!(await folderExists(folderPathFull))) + throw { + code: 404, + status: 'Not Found', + message: `Folder doesn't exist.` + } await deleteFolderOnSystem(folderPathFull) @@ -414,13 +441,19 @@ const saveFile = async ( .join(driveFilesPath, filePath) .replace(new RegExp('/', 'g'), path.sep) - if (!filePathFull.includes(driveFilesPath)) { - throw new Error('Cannot put file outside drive.') - } + if (!filePathFull.includes(driveFilesPath)) + throw { + code: 400, + status: 'Bad Request', + message: `Can't put file outside drive.` + } - if (await fileExists(filePathFull)) { - throw new Error('File already exists.') - } + if (await fileExists(filePathFull)) + throw { + code: 409, + status: 'Conflict', + message: 'File already exists.' + } const folderPath = path.dirname(filePathFull) await createFolder(folderPath) @@ -436,13 +469,19 @@ const addFolder = async (folderPath: string): Promise => { .join(drivePath, folderPath) .replace(new RegExp('/', 'g'), path.sep) - if (!folderPathFull.includes(drivePath)) { - throw new Error('Cannot put folder outside drive.') - } + if (!folderPathFull.includes(drivePath)) + throw { + code: 400, + status: 'Bad Request', + message: `Can't put folder outside drive.` + } - if (await folderExists(folderPathFull)) { - throw new Error('Folder already exists.') - } + if (await folderExists(folderPathFull)) + throw { + code: 409, + status: 'Conflict', + message: 'Folder already exists.' + } await createFolder(folderPathFull) @@ -463,30 +502,46 @@ const rename = async ( .join(drivePath, newPath) .replace(new RegExp('/', 'g'), path.sep) - if (!oldPathFull.includes(drivePath)) { - throw new Error('Old path is outside drive.') - } + if (!oldPathFull.includes(drivePath)) + throw { + code: 400, + status: 'Bad Request', + message: `Old path can't be outside of drive.` + } - if (!newPathFull.includes(drivePath)) { - throw new Error('New path is outside drive.') - } + if (!newPathFull.includes(drivePath)) + throw { + code: 400, + status: 'Bad Request', + message: `New path can't be outside of drive.` + } - if (await folderExists(oldPathFull)) { - if (await folderExists(newPathFull)) { - throw new Error('Folder already exists.') - } else moveFile(oldPathFull, newPathFull) + if (await isFolder(oldPathFull)) { + if (await folderExists(newPathFull)) + throw { + code: 409, + status: 'Conflict', + message: 'Folder with new name already exists.' + } + else moveFile(oldPathFull, newPathFull) + return { status: 'success' } + } else if (await fileExists(oldPathFull)) { + if (await fileExists(newPathFull)) + throw { + code: 409, + status: 'Conflict', + message: 'File with new name already exists.' + } + else moveFile(oldPathFull, newPathFull) return { status: 'success' } } - if (await fileExists(oldPathFull)) { - if (await fileExists(newPathFull)) { - throw new Error('File already exists.') - } else moveFile(oldPathFull, newPathFull) - return { status: 'success' } + throw { + code: 404, + status: 'Not Found', + message: 'No file/folder found for provided path.' } - - throw new Error('No file/folder found for provided path.') } const updateFile = async ( @@ -499,13 +554,19 @@ const updateFile = async ( .join(driveFilesPath, filePath) .replace(new RegExp('/', 'g'), path.sep) - if (!filePathFull.includes(driveFilesPath)) { - throw new Error('Cannot modify file outside drive.') - } + if (!filePathFull.includes(driveFilesPath)) + throw { + code: 400, + status: 'Bad Request', + message: `Can't modify file outside drive.` + } - if (!(await fileExists(filePathFull))) { - throw new Error(`File doesn't exist.`) - } + if (!(await fileExists(filePathFull))) + throw { + code: 404, + status: 'Not Found', + message: `File doesn't exist.` + } await moveFile(multerFile.path, filePathFull) diff --git a/api/src/routes/api/drive.ts b/api/src/routes/api/drive.ts index c1e94c0..e404821 100644 --- a/api/src/routes/api/drive.ts +++ b/api/src/routes/api/drive.ts @@ -121,7 +121,11 @@ driveRouter.get('/file', async (req, res) => { try { await controller.getFile(req, query._filePath) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } }) @@ -134,7 +138,11 @@ driveRouter.get('/folder', async (req, res) => { const response = await controller.getFolder(query._folderPath) res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } }) @@ -147,7 +155,11 @@ driveRouter.delete('/file', async (req, res) => { const response = await controller.deleteFile(query._filePath) res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } }) @@ -160,7 +172,11 @@ driveRouter.delete('/folder', async (req, res) => { const response = await controller.deleteFolder(query._folderPath) res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } }) @@ -187,7 +203,12 @@ driveRouter.post( res.send(response) } catch (err: any) { await deleteFile(req.file.path) - res.status(403).send(err.toString()) + + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } } ) @@ -201,7 +222,11 @@ driveRouter.post('/folder', async (req, res) => { const response = await controller.addFolder(body) res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } }) @@ -228,7 +253,12 @@ driveRouter.patch( res.send(response) } catch (err: any) { await deleteFile(req.file.path) - res.status(403).send(err.toString()) + + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } } ) @@ -242,7 +272,11 @@ driveRouter.post('/rename', async (req, res) => { const response = await controller.rename(body) res.send(response) } catch (err: any) { - res.status(403).send(err.toString()) + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err.message) } }) diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts index 5651ec3..4e8a875 100644 --- a/api/src/routes/api/spec/drive.spec.ts +++ b/api/src/routes/api/spec/drive.spec.ts @@ -549,29 +549,29 @@ describe('drive', () => { expect(res.body).toEqual({}) }) - it('should respond with Forbidden if folder is not present', async () => { + it('should respond with Not Found if folder is not present', async () => { const res = await request(app) .get(getFolderApi) .auth(accessToken, { type: 'bearer' }) .query({ _folderPath: `/my/path/code-${generateTimestamp()}` }) - .expect(403) + .expect(404) - expect(res.text).toEqual(`Error: Folder doesn't exist.`) + expect(res.text).toEqual(`Folder doesn't exist.`) expect(res.body).toEqual({}) }) - it('should respond with Forbidden if folderPath outside Drive', async () => { + it('should respond with Bad Request if folderPath outside Drive', async () => { const res = await request(app) .get(getFolderApi) .auth(accessToken, { type: 'bearer' }) .query({ _folderPath: '/../path/code.sas' }) - .expect(403) + .expect(400) - expect(res.text).toEqual('Error: Cannot get folder outside drive.') + expect(res.text).toEqual(`Can't get folder outside drive.`) expect(res.body).toEqual({}) }) - it('should respond with Forbidden if folderPath is of a file', async () => { + it('should respond with Bad Request if folderPath is of a file', async () => { const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas') const filePath = '/my/path/code.sas' @@ -582,9 +582,9 @@ describe('drive', () => { .get(getFolderApi) .auth(accessToken, { type: 'bearer' }) .query({ _folderPath: filePath }) - .expect(403) + .expect(400) - expect(res.text).toEqual('Error: Not a Folder.') + expect(res.text).toEqual('Not a Folder.') expect(res.body).toEqual({}) }) }) @@ -609,24 +609,28 @@ describe('drive', () => { }) }) - it('should respond with Forbidden if the folder already exists', async () => { + it('should respond with Conflict if the folder already exists', async () => { await createFolder(path.join(pathToDrive, '/post/folder')) const res = await request(app) .post(folderApi) .auth(accessToken, { type: 'bearer' }) .send({ folderPath: '/post/folder' }) + .expect(409) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual(`Folder already exists.`) + + expect(res.statusCode).toEqual(409) }) - it('should respond with Forbidden if the folderPath is outside drive', async () => { + it('should respond with Bad Request if the folderPath is outside drive', async () => { const res = await request(app) .post(folderApi) .auth(accessToken, { type: 'bearer' }) .send({ folderPath: '../sample' }) + .expect(400) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual(`Can't put folder outside drive.`) }) }) @@ -648,22 +652,24 @@ describe('drive', () => { }) }) - it('should respond with Forbidden if the folder does not exists', async () => { + it('should respond with Not Found if the folder does not exists', async () => { const res = await request(app) .delete(folderApi) .auth(accessToken, { type: 'bearer' }) .query({ _folderPath: 'notExists' }) + .expect(404) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual(`Folder doesn't exist.`) }) - it('should respond with Forbidden if the folderPath is outside drive', async () => { + it('should respond with Bad Request if the folderPath is outside drive', async () => { const res = await request(app) .delete(folderApi) .auth(accessToken, { type: 'bearer' }) .query({ _folderPath: '../outsideDrive' }) + .expect(400) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual(`Can't delete folder outside drive.`) }) }) }) @@ -711,7 +717,7 @@ describe('drive', () => { expect(res.body).toEqual({}) }) - it('should respond with Forbidden if file is already present', async () => { + it('should respond with Conflict if file is already present', async () => { const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const pathToUpload = `/my/path/code-${generateTimestamp()}.sas` @@ -726,13 +732,13 @@ describe('drive', () => { .auth(accessToken, { type: 'bearer' }) .field('filePath', pathToUpload) .attach('file', fileToAttachPath) - .expect(403) + .expect(409) - expect(res.text).toEqual('Error: File already exists.') + expect(res.text).toEqual('File already exists.') expect(res.body).toEqual({}) }) - it('should respond with Forbidden if filePath outside Drive', async () => { + it('should respond with Bad Request if filePath outside Drive', async () => { const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const pathToUpload = '/../path/code.sas' @@ -741,9 +747,9 @@ describe('drive', () => { .auth(accessToken, { type: 'bearer' }) .field('filePath', pathToUpload) .attach('file', fileToAttachPath) - .expect(403) + .expect(400) - expect(res.text).toEqual('Error: Cannot put file outside drive.') + expect(res.text).toEqual(`Can't put file outside drive.`) expect(res.body).toEqual({}) }) @@ -878,19 +884,19 @@ describe('drive', () => { expect(res.body).toEqual({}) }) - it('should respond with Forbidden if file is not present', async () => { + it('should respond with Not Found if file is not present', async () => { const res = await request(app) .patch('/SASjsApi/drive/file') .auth(accessToken, { type: 'bearer' }) .field('filePath', `/my/path/code-3.sas`) .attach('file', path.join(__dirname, 'files', 'sample.sas')) - .expect(403) + .expect(404) - expect(res.text).toEqual(`Error: File doesn't exist.`) + expect(res.text).toEqual(`File doesn't exist.`) expect(res.body).toEqual({}) }) - it('should respond with Forbidden if filePath outside Drive', async () => { + it('should respond with Bad Request if filePath outside Drive', async () => { const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const pathToUpload = '/../path/code.sas' @@ -899,9 +905,9 @@ describe('drive', () => { .auth(accessToken, { type: 'bearer' }) .field('filePath', pathToUpload) .attach('file', fileToAttachPath) - .expect(403) + .expect(400) - expect(res.text).toEqual('Error: Cannot modify file outside drive.') + expect(res.text).toEqual(`Can't modify file outside drive.`) expect(res.body).toEqual({}) }) @@ -1006,25 +1012,25 @@ describe('drive', () => { expect(res.body).toEqual({}) }) - it('should respond with Forbidden if file is not present', async () => { + it('should respond with Not Found if file is not present', async () => { const res = await request(app) .get('/SASjsApi/drive/file') .auth(accessToken, { type: 'bearer' }) .query({ _filePath: `/my/path/code-4.sas` }) - .expect(403) + .expect(404) - expect(res.text).toEqual(`Error: File doesn't exist.`) + expect(res.text).toEqual(`File doesn't exist.`) expect(res.body).toEqual({}) }) - it('should respond with Forbidden if filePath outside Drive', async () => { + it('should respond with Bad Request if filePath outside Drive', async () => { const res = await request(app) .get('/SASjsApi/drive/file') .auth(accessToken, { type: 'bearer' }) .query({ _filePath: '/../path/code.sas' }) - .expect(403) + .expect(400) - expect(res.text).toEqual('Error: Cannot get file outside drive.') + expect(res.text).toEqual(`Can't get file outside drive.`) expect(res.body).toEqual({}) }) @@ -1093,54 +1099,59 @@ describe('drive', () => { }) }) - it('should respond with forbidden if the oldPath is outside drive', async () => { + it('should respond with Bad Request if the oldPath is outside drive', async () => { const res = await request(app) .post(renameApi) .auth(accessToken, { type: 'bearer' }) .send({ oldPath: '../outside', newPath: 'renamed' }) + .expect(400) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual(`Old path can't be outside of drive.`) }) - it('should respond with forbidden if the newPath is outside drive', async () => { + it('should respond with Bad Request if the newPath is outside drive', async () => { const res = await request(app) .post(renameApi) .auth(accessToken, { type: 'bearer' }) .send({ oldPath: 'older', newPath: '../outside' }) + .expect(400) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual(`New path can't be outside of drive.`) }) - it('should respond with forbidden if the folder does not exist', async () => { + it('should respond with Not Found if the folder does not exist', async () => { const res = await request(app) .post(renameApi) .auth(accessToken, { type: 'bearer' }) .send({ oldPath: '/rename/not exists', newPath: '/rename/renamed' }) + .expect(404) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual('No file/folder found for provided path.') }) - it('should respond with forbidden if the folder already exists', async () => { + it('should respond with Conflict if the folder already exists', async () => { await createFolder(path.join(pathToDrive, 'rename', 'folder')) await createFolder(path.join(pathToDrive, 'rename', 'exists')) const res = await request(app) .post(renameApi) .auth(accessToken, { type: 'bearer' }) .send({ oldPath: '/rename/folder', newPath: '/rename/exists' }) + .expect(409) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual('Folder with new name already exists.') }) - it('should respond with forbidden if the file does not exist', async () => { + it('should respond with Not Found if the file does not exist', async () => { const res = await request(app) .post(renameApi) .auth(accessToken, { type: 'bearer' }) .send({ oldPath: '/rename/file.txt', newPath: '/rename/renamed.txt' }) + .expect(404) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual('No file/folder found for provided path.') }) - it('should respond with forbidden if the file already exists', async () => { + it('should respond with Conflict if the file already exists', async () => { await createFile( path.join(pathToDrive, 'rename', 'file.txt'), 'some file content' @@ -1153,8 +1164,9 @@ describe('drive', () => { .post(renameApi) .auth(accessToken, { type: 'bearer' }) .send({ oldPath: '/rename/file.txt', newPath: '/rename/exists.txt' }) + .expect(409) - expect(res.statusCode).toEqual(403) + expect(res.text).toEqual('File with new name already exists.') }) }) }) From 99172cd9edaf3608239bdfac96e4e4cca1633bea Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 22 Jul 2022 22:18:03 +0500 Subject: [PATCH 23/29] chore: add specs --- api/src/routes/api/spec/drive.spec.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/src/routes/api/spec/drive.spec.ts b/api/src/routes/api/spec/drive.spec.ts index 4e8a875..dfc5f8b 100644 --- a/api/src/routes/api/spec/drive.spec.ts +++ b/api/src/routes/api/spec/drive.spec.ts @@ -1099,6 +1099,26 @@ describe('drive', () => { }) }) + it('should respond with Bad Request if the oldPath is missing', async () => { + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ newPath: 'newPath' }) + .expect(400) + + expect(res.text).toEqual(`\"oldPath\" is required`) + }) + + it('should respond with Bad Request if the newPath is missing', async () => { + const res = await request(app) + .post(renameApi) + .auth(accessToken, { type: 'bearer' }) + .send({ oldPath: 'oldPath' }) + .expect(400) + + expect(res.text).toEqual(`\"newPath\" is required`) + }) + it('should respond with Bad Request if the oldPath is outside drive', async () => { const res = await request(app) .post(renameApi) From 420a61a5a6b11dcb5eb0a652ea9cecea5c3bee5f Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 25 Jul 2022 15:01:04 +0500 Subject: [PATCH 24/29] feat(web): add difference view editor in studio --- web/src/containers/Studio/editor.tsx | 111 ++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 18 deletions(-) diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx index f84495b..f206f69 100644 --- a/web/src/containers/Studio/editor.tsx +++ b/web/src/containers/Studio/editor.tsx @@ -18,8 +18,19 @@ import { } from '@mui/material' import { styled } from '@mui/material/styles' -import { RocketLaunch, MoreVert, Save, SaveAs } from '@mui/icons-material' -import Editor, { EditorDidMount } from 'react-monaco-editor' +import { + RocketLaunch, + MoreVert, + Save, + SaveAs, + Difference, + Edit +} from '@mui/icons-material' +import Editor, { + MonacoDiffEditor, + DiffEditorDidMount, + EditorDidMount +} from 'react-monaco-editor' import { TabContext, TabList, TabPanel } from '@mui/lab' import { AppContext, RunTimeType } from '../../context/appContext' @@ -71,14 +82,22 @@ const SASjsEditor = ({ const [selectedRunTime, setSelectedRunTime] = useState('') const [selectedFileExtension, setSelectedFileExtension] = useState('') const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false) + const [showDiff, setShowDiff] = useState(false) const editorRef = useRef(null as any) + const diffEditorRef = useRef(null as any) + const handleEditorDidMount: EditorDidMount = (editor) => { editor.focus() editorRef.current = editor } + const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => { + diffEditor.focus() + diffEditorRef.current = diffEditor + } + useEffect(() => { setRunTimes(Object.values(appContext.runTimes)) }, [appContext.runTimes]) @@ -226,6 +245,8 @@ const SASjsEditor = ({ - setFileContent(val)} - /> + {showDiff ? ( + setFileContent(val)} + /> + ) : ( + setFileContent(val)} + /> + )}
    ) : ( @@ -286,6 +319,8 @@ const SASjsEditor = ({ handleRunBtnClick={handleRunBtnClick} /> - setFileContent(val)} - /> + {showDiff ? ( + setFileContent(val)} + /> + ) : ( + setFileContent(val)} + /> + )}

    > prevFileContent: string currentFileContent: string selectedFilePath: string @@ -441,6 +490,8 @@ type FileMenuProps = { } const FileMenu = ({ + showDiff, + setShowDiff, prevFileContent, currentFileContent, selectedFilePath, @@ -490,6 +541,16 @@ const FileMenu = ({ open={!!anchorEl} onClose={() => handleMenu()} > + + +