mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b88c911527 | ||
|
|
8b12f31060 | ||
|
|
e65cba9af0 | ||
|
|
a9c9b734f5 | ||
|
|
39da41c9f1 | ||
| 662b2ca36a | |||
| 16b7aa6abb | |||
| 4560ef942f | |||
| 06d3b17154 | |||
| d6651bbdbe | |||
| b9d032f148 | |||
|
|
70655e74d3 | ||
|
|
cb82fea0d8 | ||
| b9a596616d | |||
|
|
72a5393be3 | ||
|
|
769a840e9f | ||
| 730c7c52ac | |||
| ee2db276bb | |||
|
|
d0a24aacb6 | ||
|
|
57dfdf89a4 | ||
|
|
393b5eaf99 | ||
|
|
7477326b22 | ||
|
|
76bf84316e | ||
|
|
e355276e44 | ||
|
|
a3a9e3bd9f | ||
|
|
9f06080348 | ||
|
|
4bbf9cfdb3 | ||
|
|
e8e71fcde9 | ||
|
|
e63271a67a | ||
| 7633608318 | |||
|
|
e67d27d264 | ||
|
|
53033ccc96 | ||
|
|
6131ed1cbe | ||
|
|
5d624e3399 | ||
| ee17d37aa1 | |||
| 572fe22d50 | |||
| 091268bf58 | |||
| 71a4a48443 | |||
| 3b188cd724 | |||
| eeba2328c0 | |||
| 0a0ba2cca5 | |||
|
|
476f834a80 | ||
|
|
8b8739a873 | ||
| bce83cb6fb | |||
| 3a3c90d9e6 | |||
|
|
e63eaa5302 | ||
|
|
65de1bb175 | ||
|
|
a5ee2f2923 | ||
| 98ea2ac9b9 | |||
|
|
e94c56b23f | ||
|
|
64f80e958d | ||
| bd97363c13 | |||
| 02e88ae728 | |||
| 882bedd5d5 | |||
| 8780b800a3 | |||
| 4c11082796 | |||
| a9b25b8880 | |||
| b06993ab9e | |||
|
|
f736e67517 | ||
|
|
0f4a60c0c7 | ||
|
|
f8bb7327a8 | ||
|
|
abce135da2 | ||
|
|
a6c014946a | ||
| f27ac51fc4 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ node_modules/
|
|||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
sasjs_root/
|
sasjs_root/
|
||||||
|
api/mocks/custom/*
|
||||||
|
!api/mocks/custom/.keep
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
|||||||
110
CHANGELOG.md
110
CHANGELOG.md
@@ -1,3 +1,113 @@
|
|||||||
|
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* sas9 mocker improved - public access denied scenario ([06d3b17](https://github.com/sasjs/server/commit/06d3b1715432ea245ee755ae1dfd0579d3eb30e9))
|
||||||
|
|
||||||
|
# [0.20.0](https://github.com/sasjs/server/compare/v0.19.0...v0.20.0) (2022-09-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add support for R stored programs ([d6651bb](https://github.com/sasjs/server/commit/d6651bbdbeee5067f53c36e69a0eefa973c523b6))
|
||||||
|
|
||||||
|
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
|
||||||
|
|
||||||
|
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add option for program launch in context menu ([ee2db27](https://github.com/sasjs/server/commit/ee2db276bb0bbd522f758e0b66f7e7b2f4afd9d5))
|
||||||
|
|
||||||
|
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](https://github.com/sasjs/server/commit/393b5eaf990049c39eecf2b9e8dd21a001b6e298))
|
||||||
|
|
||||||
|
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* invalid JS logic ([9f06080](https://github.com/sasjs/server/commit/9f06080348aed076f8188a26fb4890d38a5a3510))
|
||||||
|
|
||||||
|
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* making SASINITIALFOLDER option windows only. Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](https://github.com/sasjs/server/commit/e63271a67a0deb3059a5f2bec1854efee5a6e5a5))
|
||||||
|
|
||||||
|
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* addition of SASINITIALFOLDER startup option. Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](https://github.com/sasjs/server/commit/a5ee2f292384f90e9d95d003d652311c0d91a7a7))
|
||||||
|
|
||||||
|
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo mistake ([ee17d37](https://github.com/sasjs/server/commit/ee17d37aa188b0ca43cea0e89d6cd1a566b765cb))
|
||||||
|
|
||||||
|
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow underscores in file name ([bce83cb](https://github.com/sasjs/server/commit/bce83cb6fbc98f8198564c9399821f5829acc767))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add the functionality of saving file by ctrl + s in editor ([3a3c90d](https://github.com/sasjs/server/commit/3a3c90d9e690ac5267bf1acc834b5b5c5b4dadb6))
|
||||||
|
|
||||||
|
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](https://github.com/sasjs/server/commit/98ea2ac9b98631605e39e5900e533727ea0e3d85))
|
||||||
|
|
||||||
|
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py ([882bedd](https://github.com/sasjs/server/commit/882bedd5d5da22de6ed45c03d0a261aadfb3a33c))
|
||||||
|
* update content for code.sas file ([02e88ae](https://github.com/sasjs/server/commit/02e88ae7280d020a753bc2c095a931c79ac392d1))
|
||||||
|
* update default content type for python and js runtimes ([8780b80](https://github.com/sasjs/server/commit/8780b800a34aa618631821e5d97e26e8b0f15806))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implement the logic for running python stored programs ([b06993a](https://github.com/sasjs/server/commit/b06993ab9ea24b28d9e553763187387685aaa666))
|
||||||
|
|
||||||
|
## [0.15.3](https://github.com/sasjs/server/compare/v0.15.2...v0.15.3) (2022-08-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding proc printto in precode to enable print output in log. Closes [#253](https://github.com/sasjs/server/issues/253) ([f8bb732](https://github.com/sasjs/server/commit/f8bb7327a8a4649ac77bb6237e31cea075d46bb9))
|
||||||
|
|
||||||
|
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* remove vulnerabitities ([f27ac51](https://github.com/sasjs/server/commit/f27ac51fc4beb21070d0ab551cfdaec1f6ba39e0))
|
||||||
|
|
||||||
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)
|
## [0.15.1](https://github.com/sasjs/server/compare/v0.15.0...v0.15.1) (2022-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -64,12 +64,30 @@ Example contents of a `.env` file:
|
|||||||
# Server mode is multi-user and suitable for intranet / internet use
|
# Server mode is multi-user and suitable for intranet / internet use
|
||||||
MODE=
|
MODE=
|
||||||
|
|
||||||
|
# A comma separated string that defines the available runTimes.
|
||||||
|
# Priority is given to the runtime that comes first in the string.
|
||||||
|
# Possible options at the moment are sas, js, py and r
|
||||||
|
|
||||||
|
# This string sets the priority of the available analytic runtimes
|
||||||
|
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
|
||||||
|
# For each option provided, there should be a corresponding path,
|
||||||
|
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
|
||||||
|
# Priority is given to runtimes earlier in the string
|
||||||
|
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
|
||||||
|
RUN_TIMES=
|
||||||
|
|
||||||
# Path to SAS executable (sas.exe / sas.sh)
|
# Path to SAS executable (sas.exe / sas.sh)
|
||||||
SAS_PATH=/path/to/sas/executable.exe
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
|
||||||
# Path to Node.js executable
|
# Path to Node.js executable
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
|
||||||
|
# Path to Python executable
|
||||||
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
|
||||||
|
# Path to R executable
|
||||||
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
# Path to working directory
|
# Path to working directory
|
||||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
@@ -81,6 +99,9 @@ PROTOCOL=
|
|||||||
# default: 5000
|
# default: 5000
|
||||||
PORT=
|
PORT=
|
||||||
|
|
||||||
|
# options: [sas9|sasviya]
|
||||||
|
# If not present, mocking function is disabled
|
||||||
|
MOCK_SERVERTYPE=
|
||||||
|
|
||||||
#
|
#
|
||||||
## Additional SAS Options
|
## Additional SAS Options
|
||||||
@@ -139,13 +160,6 @@ LOG_FORMAT_MORGAN=
|
|||||||
# This location is for server logs with classical UNIX logrotate behavior
|
# This location is for server logs with classical UNIX logrotate behavior
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
|
||||||
# Priority is given to the runtime that comes first in the string.
|
|
||||||
# Possible options at the moment are sas and js
|
|
||||||
|
|
||||||
# options: [sas,js|js,sas|sas|js] default:sas
|
|
||||||
RUN_TIMES=
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Persisting the Session
|
## Persisting the Session
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ HELMET_COEP=[true|false] if omitted HELMET default will be used
|
|||||||
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
|||||||
0
api/mocks/custom/.keep
Normal file
0
api/mocks/custom/.keep
Normal file
1
api/mocks/generic/sas9/logged-in
Normal file
1
api/mocks/generic/sas9/logged-in
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed in.
|
||||||
1
api/mocks/generic/sas9/logged-out
Normal file
1
api/mocks/generic/sas9/logged-out
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed out.
|
||||||
30
api/mocks/generic/sas9/login
Normal file
30
api/mocks/generic/sas9/login
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
||||||
|
<!--form container-->
|
||||||
|
<input type="hidden" name="lt" value="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" aria-hidden="true" />
|
||||||
|
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
||||||
|
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
||||||
|
|
||||||
|
<span class="userid">
|
||||||
|
|
||||||
|
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
|
||||||
|
</span>
|
||||||
|
<span class="password">
|
||||||
|
|
||||||
|
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
1
api/mocks/generic/sas9/public-access-denied
Normal file
1
api/mocks/generic/sas9/public-access-denied
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Public access has been denied.
|
||||||
1
api/mocks/generic/sas9/sas-stored-process
Normal file
1
api/mocks/generic/sas9/sas-stored-process
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"title": "Log Off SAS Demo User"
|
||||||
503
api/package-lock.json
generated
503
api/package-lock.json
generated
@@ -23,7 +23,7 @@
|
|||||||
"mongoose": "^6.0.12",
|
"mongoose": "^6.0.12",
|
||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.5-lts.1",
|
||||||
"rotating-file-stream": "^3.0.4",
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
@@ -2184,9 +2184,9 @@
|
|||||||
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
|
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/whatwg-url": {
|
"node_modules/@types/whatwg-url": {
|
||||||
"version": "8.2.1",
|
"version": "8.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||||
"integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==",
|
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/webidl-conversions": "*"
|
"@types/webidl-conversions": "*"
|
||||||
@@ -2834,9 +2834,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bson": {
|
"node_modules/bson": {
|
||||||
"version": "4.5.4",
|
"version": "4.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/bson/-/bson-4.6.5.tgz",
|
||||||
"integrity": "sha512-wIt0bPACnx8Ju9r6IsS2wVtGDHBr9Dxb+U29A1YED2pu8XOhS8aKjOnLZ8sxyXkPwanoK7iWWVhS1+coxde6xA==",
|
"integrity": "sha512-uqrgcjyOaZsHfz7ea8zLRCLe1u+QGUSzMZmvXqO24CDW7DWoW1qiN9folSwa7hSneTSgM2ykDIzF5kcQQ8cwNw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.6.0"
|
"buffer": "^5.6.0"
|
||||||
},
|
},
|
||||||
@@ -2903,38 +2903,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "0.2.14",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dicer": "0.2.5",
|
"streamsearch": "^1.1.0"
|
||||||
"readable-stream": "1.1.x"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=10.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/busboy/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/busboy/node_modules/readable-stream": {
|
|
||||||
"version": "1.1.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
|
||||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
|
||||||
"dependencies": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.1",
|
|
||||||
"isarray": "0.0.1",
|
|
||||||
"string_decoder": "~0.10.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/busboy/node_modules/string_decoder": {
|
|
||||||
"version": "0.10.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
|
||||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
|
||||||
},
|
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||||
@@ -3566,39 +3544,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dicer": {
|
|
||||||
"version": "0.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
|
||||||
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
|
|
||||||
"dependencies": {
|
|
||||||
"readable-stream": "1.1.x",
|
|
||||||
"streamsearch": "0.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dicer/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/dicer/node_modules/readable-stream": {
|
|
||||||
"version": "1.1.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
|
||||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
|
||||||
"dependencies": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.1",
|
|
||||||
"isarray": "0.0.1",
|
|
||||||
"string_decoder": "~0.10.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dicer/node_modules/string_decoder": {
|
|
||||||
"version": "0.10.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
|
||||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
|
||||||
},
|
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
@@ -4946,6 +4891,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ip": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -6788,9 +6738,9 @@
|
|||||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
},
|
},
|
||||||
"node_modules/kareem": {
|
"node_modules/kareem": {
|
||||||
"version": "2.3.2",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz",
|
||||||
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
|
"integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA=="
|
||||||
},
|
},
|
||||||
"node_modules/kleur": {
|
"node_modules/kleur": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@@ -7093,13 +7043,14 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/mongodb": {
|
"node_modules/mongodb": {
|
||||||
"version": "4.1.4",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.8.1.tgz",
|
||||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
"integrity": "sha512-/NyiM3Ox9AwP5zrfT9TXjRKDJbXlLaUDQ9Rg//2lbg8D2A8GXV0VidYYnA/gfdK6uwbnL4FnAflH7FbGw3TS7w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bson": "^4.5.4",
|
"bson": "^4.6.5",
|
||||||
"denque": "^2.0.1",
|
"denque": "^2.0.1",
|
||||||
"mongodb-connection-string-url": "^2.1.0"
|
"mongodb-connection-string-url": "^2.5.2",
|
||||||
|
"socks": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.9.0"
|
"node": ">=12.9.0"
|
||||||
@@ -7109,21 +7060,40 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mongodb-connection-string-url": {
|
"node_modules/mongodb-connection-string-url": {
|
||||||
"version": "2.1.0",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
|
||||||
"integrity": "sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==",
|
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/whatwg-url": "^8.2.1",
|
"@types/whatwg-url": "^8.2.1",
|
||||||
"whatwg-url": "^9.1.0"
|
"whatwg-url": "^11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||||
"version": "9.1.0",
|
"version": "11.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||||
"integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==",
|
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tr46": "^2.1.0",
|
"tr46": "^3.0.0",
|
||||||
"webidl-conversions": "^6.1.0"
|
"webidl-conversions": "^7.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -7221,19 +7191,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mongoose": {
|
"node_modules/mongoose": {
|
||||||
"version": "6.0.12",
|
"version": "6.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.2.tgz",
|
||||||
"integrity": "sha512-BvsZk7zEEhb1AgQFLtxN9C+7qgy5edRuA3ZDDwHU+kHG/HM44vI6FdKV5m6HVdAUeCHHQTiVv+YQh8BRsToSHw==",
|
"integrity": "sha512-3CFDrSLtK2qjM1pZeZpLTUyqPRkc11Iuh74ZrwS4IwEJ3K2PqGnmyPLw7ex4Kzu37ujIMp3MAuiBlUjfrcb6hw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bson": "^4.2.2",
|
"bson": "^4.6.5",
|
||||||
"kareem": "2.3.2",
|
"kareem": "2.4.1",
|
||||||
"mongodb": "4.1.3",
|
"mongodb": "4.8.1",
|
||||||
"mpath": "0.8.4",
|
"mpath": "0.9.0",
|
||||||
"mquery": "4.0.0",
|
"mquery": "4.0.3",
|
||||||
"ms": "2.1.2",
|
"ms": "2.1.3",
|
||||||
"regexp-clone": "1.0.0",
|
"sift": "16.0.0"
|
||||||
"sift": "13.5.2",
|
|
||||||
"sliced": "1.0.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -7255,26 +7223,10 @@
|
|||||||
"mongoose": ">=4"
|
"mongoose": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mongoose/node_modules/mongodb": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-lHvTqODBiSpuqjpCj48DOyYWS6Iq6ElJNUiH9HWdQtONyOfjgsKzJULipWduMGsSzaNO4nFi/kmlMFCLvjox/Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"bson": "^4.5.2",
|
|
||||||
"denque": "^2.0.1",
|
|
||||||
"mongodb-connection-string-url": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.9.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"saslprep": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongoose/node_modules/ms": {
|
"node_modules/mongoose/node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
"node_modules/morgan": {
|
"node_modules/morgan": {
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
@@ -7300,30 +7252,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mpath": {
|
"node_modules/mpath": {
|
||||||
"version": "0.8.4",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||||
"integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==",
|
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mquery": {
|
"node_modules/mquery": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
|
||||||
"integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==",
|
"integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "4.x",
|
"debug": "4.x"
|
||||||
"regexp-clone": "^1.0.0",
|
|
||||||
"sliced": "1.0.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mquery/node_modules/debug": {
|
"node_modules/mquery/node_modules/debug": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
@@ -7347,22 +7297,20 @@
|
|||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
},
|
},
|
||||||
"node_modules/multer": {
|
"node_modules/multer": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.5-lts.1",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
||||||
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
||||||
"deprecated": "Multer 1.x is affected by CVE-2022-24434. This is fixed in v1.4.4-lts.1 which drops support for versions of Node.js before 6. Please upgrade to at least Node.js 6 and version 1.4.4-lts.1 of Multer. If you need support for older versions of Node.js, we are open to accepting patches that would fix the CVE on the main 1.x release line, whilst maintaining compatibility with Node.js 0.10.",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
"busboy": "^0.2.11",
|
"busboy": "^1.0.0",
|
||||||
"concat-stream": "^1.5.2",
|
"concat-stream": "^1.5.2",
|
||||||
"mkdirp": "^0.5.4",
|
"mkdirp": "^0.5.4",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"on-finished": "^2.3.0",
|
|
||||||
"type-is": "^1.6.4",
|
"type-is": "^1.6.4",
|
||||||
"xtend": "^4.0.0"
|
"xtend": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/multer/node_modules/mkdirp": {
|
"node_modules/multer/node_modules/mkdirp": {
|
||||||
@@ -8353,11 +8301,6 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/regexp-clone": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
|
|
||||||
},
|
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -8606,9 +8549,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sift": {
|
"node_modules/sift": {
|
||||||
"version": "13.5.2",
|
"version": "16.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz",
|
||||||
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
|
"integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ=="
|
||||||
},
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@@ -8706,10 +8649,27 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sliced": {
|
"node_modules/smart-buffer": {
|
||||||
"version": "1.0.1",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
|
||||||
|
"dependencies": {
|
||||||
|
"ip": "^2.0.0",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.13.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
@@ -8808,11 +8768,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/streamsearch": {
|
"node_modules/streamsearch": {
|
||||||
"version": "0.1.2",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=",
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
@@ -9296,6 +9256,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
|
||||||
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
|
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.1"
|
"punycode": "^2.1.1"
|
||||||
},
|
},
|
||||||
@@ -9719,6 +9680,7 @@
|
|||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
||||||
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
|
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.4"
|
"node": ">=10.4"
|
||||||
}
|
}
|
||||||
@@ -11724,9 +11686,9 @@
|
|||||||
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
|
"integrity": "sha512-XAahCdThVuCFDQLT7R7Pk/vqeObFNL3YqRyFZg+AqAP/W1/w3xHaIxuW7WszQqTbIBOPRcItYJIou3i/mppu3Q=="
|
||||||
},
|
},
|
||||||
"@types/whatwg-url": {
|
"@types/whatwg-url": {
|
||||||
"version": "8.2.1",
|
"version": "8.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
|
||||||
"integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==",
|
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/webidl-conversions": "*"
|
"@types/webidl-conversions": "*"
|
||||||
@@ -12240,9 +12202,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bson": {
|
"bson": {
|
||||||
"version": "4.5.4",
|
"version": "4.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/bson/-/bson-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/bson/-/bson-4.6.5.tgz",
|
||||||
"integrity": "sha512-wIt0bPACnx8Ju9r6IsS2wVtGDHBr9Dxb+U29A1YED2pu8XOhS8aKjOnLZ8sxyXkPwanoK7iWWVhS1+coxde6xA==",
|
"integrity": "sha512-uqrgcjyOaZsHfz7ea8zLRCLe1u+QGUSzMZmvXqO24CDW7DWoW1qiN9folSwa7hSneTSgM2ykDIzF5kcQQ8cwNw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"buffer": "^5.6.0"
|
"buffer": "^5.6.0"
|
||||||
}
|
}
|
||||||
@@ -12283,35 +12245,11 @@
|
|||||||
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
|
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
|
||||||
},
|
},
|
||||||
"busboy": {
|
"busboy": {
|
||||||
"version": "0.2.14",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
"integrity": "sha512-InWFDomvlkEj+xWLBfU3AvnbVYqeTWmQopiW0tWWEy5yehYm2YkGEc59sUmw/4ty5Zj/b0WHGs1LgecuBSBGrg==",
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"dicer": "0.2.5",
|
"streamsearch": "^1.1.0"
|
||||||
"readable-stream": "1.1.x"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"isarray": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
|
||||||
},
|
|
||||||
"readable-stream": {
|
|
||||||
"version": "1.1.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
|
||||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
|
||||||
"requires": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.1",
|
|
||||||
"isarray": "0.0.1",
|
|
||||||
"string_decoder": "~0.10.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"string_decoder": {
|
|
||||||
"version": "0.10.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
|
||||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bytes": {
|
"bytes": {
|
||||||
@@ -12813,38 +12751,6 @@
|
|||||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"dicer": {
|
|
||||||
"version": "0.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
|
|
||||||
"integrity": "sha512-FDvbtnq7dzlPz0wyYlOExifDEZcu8h+rErEXgfxqmLfRfC/kJidEFh4+effJRO3P0xmfqyPbSMG0LveNRfTKVg==",
|
|
||||||
"requires": {
|
|
||||||
"readable-stream": "1.1.x",
|
|
||||||
"streamsearch": "0.1.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"isarray": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
|
||||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
|
||||||
},
|
|
||||||
"readable-stream": {
|
|
||||||
"version": "1.1.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
|
|
||||||
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
|
|
||||||
"requires": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.1",
|
|
||||||
"isarray": "0.0.1",
|
|
||||||
"string_decoder": "~0.10.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"string_decoder": {
|
|
||||||
"version": "0.10.31",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
|
||||||
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"diff": {
|
"diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
@@ -13868,6 +13774,11 @@
|
|||||||
"p-is-promise": "^3.0.0"
|
"p-is-promise": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ip": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ=="
|
||||||
|
},
|
||||||
"ipaddr.js": {
|
"ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -15248,9 +15159,9 @@
|
|||||||
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
},
|
},
|
||||||
"kareem": {
|
"kareem": {
|
||||||
"version": "2.3.2",
|
"version": "2.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.4.1.tgz",
|
||||||
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
|
"integrity": "sha512-aJ9opVoXroQUPfovYP5kaj2lM7Jn02Gw13bL0lg9v0V7SaUc0qavPs0Eue7d2DcC3NjqI6QAUElXNsuZSeM+EA=="
|
||||||
},
|
},
|
||||||
"kleur": {
|
"kleur": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@@ -15486,32 +15397,46 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"mongodb": {
|
"mongodb": {
|
||||||
"version": "4.1.4",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.8.1.tgz",
|
||||||
"integrity": "sha512-Cv/sk8on/tpvvqbEvR1h03mdyNdyvvO+WhtFlL4jrZ+DSsN/oSQHVqmJQI/sBCqqbOArFcYCAYDfyzqFwV4GSQ==",
|
"integrity": "sha512-/NyiM3Ox9AwP5zrfT9TXjRKDJbXlLaUDQ9Rg//2lbg8D2A8GXV0VidYYnA/gfdK6uwbnL4FnAflH7FbGw3TS7w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bson": "^4.5.4",
|
"bson": "^4.6.5",
|
||||||
"denque": "^2.0.1",
|
"denque": "^2.0.1",
|
||||||
"mongodb-connection-string-url": "^2.1.0",
|
"mongodb-connection-string-url": "^2.5.2",
|
||||||
"saslprep": "^1.0.3"
|
"saslprep": "^1.0.3",
|
||||||
|
"socks": "^2.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mongodb-connection-string-url": {
|
"mongodb-connection-string-url": {
|
||||||
"version": "2.1.0",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.5.3.tgz",
|
||||||
"integrity": "sha512-Qf9Zw7KGiRljWvMrrUFDdVqo46KIEiDuCzvEN97rh/PcKzk2bd6n9KuzEwBwW9xo5glwx69y1mI6s+jFUD/aIQ==",
|
"integrity": "sha512-f+/WsED+xF4B74l3k9V/XkTVj5/fxFH2o5ToKXd8Iyi5UhM+sO9u0Ape17Mvl/GkZaFtM0HQnzAG5OTmhKw+tQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/whatwg-url": "^8.2.1",
|
"@types/whatwg-url": "^8.2.1",
|
||||||
"whatwg-url": "^9.1.0"
|
"whatwg-url": "^11.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": {
|
"tr46": {
|
||||||
"version": "9.1.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
|
||||||
"integrity": "sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA==",
|
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tr46": "^2.1.0",
|
"punycode": "^2.1.1"
|
||||||
"webidl-conversions": "^6.1.0"
|
}
|
||||||
|
},
|
||||||
|
"webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||||
|
},
|
||||||
|
"whatwg-url": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||||
|
"requires": {
|
||||||
|
"tr46": "^3.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15583,36 +15508,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mongoose": {
|
"mongoose": {
|
||||||
"version": "6.0.12",
|
"version": "6.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.5.2.tgz",
|
||||||
"integrity": "sha512-BvsZk7zEEhb1AgQFLtxN9C+7qgy5edRuA3ZDDwHU+kHG/HM44vI6FdKV5m6HVdAUeCHHQTiVv+YQh8BRsToSHw==",
|
"integrity": "sha512-3CFDrSLtK2qjM1pZeZpLTUyqPRkc11Iuh74ZrwS4IwEJ3K2PqGnmyPLw7ex4Kzu37ujIMp3MAuiBlUjfrcb6hw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"bson": "^4.2.2",
|
"bson": "^4.6.5",
|
||||||
"kareem": "2.3.2",
|
"kareem": "2.4.1",
|
||||||
"mongodb": "4.1.3",
|
"mongodb": "4.8.1",
|
||||||
"mpath": "0.8.4",
|
"mpath": "0.9.0",
|
||||||
"mquery": "4.0.0",
|
"mquery": "4.0.3",
|
||||||
"ms": "2.1.2",
|
"ms": "2.1.3",
|
||||||
"regexp-clone": "1.0.0",
|
"sift": "16.0.0"
|
||||||
"sift": "13.5.2",
|
|
||||||
"sliced": "1.0.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mongodb": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-lHvTqODBiSpuqjpCj48DOyYWS6Iq6ElJNUiH9HWdQtONyOfjgsKzJULipWduMGsSzaNO4nFi/kmlMFCLvjox/Q==",
|
|
||||||
"requires": {
|
|
||||||
"bson": "^4.5.2",
|
|
||||||
"denque": "^2.0.1",
|
|
||||||
"mongodb-connection-string-url": "^2.0.0",
|
|
||||||
"saslprep": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -15645,24 +15557,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mpath": {
|
"mpath": {
|
||||||
"version": "0.8.4",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||||
"integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g=="
|
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="
|
||||||
},
|
},
|
||||||
"mquery": {
|
"mquery": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.3.tgz",
|
||||||
"integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==",
|
"integrity": "sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"debug": "4.x",
|
"debug": "4.x"
|
||||||
"regexp-clone": "^1.0.0",
|
|
||||||
"sliced": "1.0.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.2",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
}
|
}
|
||||||
@@ -15680,16 +15590,15 @@
|
|||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
},
|
},
|
||||||
"multer": {
|
"multer": {
|
||||||
"version": "1.4.4",
|
"version": "1.4.5-lts.1",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
||||||
"integrity": "sha512-2wY2+xD4udX612aMqMcB8Ws2Voq6NIUPEtD1be6m411T4uDH/VtL9i//xvcyFlTVfRdaBsk7hV5tgrGQqhuBiw==",
|
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
"busboy": "^0.2.11",
|
"busboy": "^1.0.0",
|
||||||
"concat-stream": "^1.5.2",
|
"concat-stream": "^1.5.2",
|
||||||
"mkdirp": "^0.5.4",
|
"mkdirp": "^0.5.4",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
"on-finished": "^2.3.0",
|
|
||||||
"type-is": "^1.6.4",
|
"type-is": "^1.6.4",
|
||||||
"xtend": "^4.0.0"
|
"xtend": "^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -16416,11 +16325,6 @@
|
|||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"regexp-clone": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
|
|
||||||
},
|
|
||||||
"require-directory": {
|
"require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
@@ -16605,9 +16509,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sift": {
|
"sift": {
|
||||||
"version": "13.5.2",
|
"version": "16.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/sift/-/sift-16.0.0.tgz",
|
||||||
"integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA=="
|
"integrity": "sha512-ILTjdP2Mv9V1kIxWMXeMTIRbOBrqKc4JAXmFMnFq3fKeyQ2Qwa3Dw1ubcye3vR+Y6ofA0b9gNDr/y2t6eUeIzQ=="
|
||||||
},
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
@@ -16677,10 +16581,19 @@
|
|||||||
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"sliced": {
|
"smart-buffer": {
|
||||||
"version": "1.0.1",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
|
||||||
|
},
|
||||||
|
"socks": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA==",
|
||||||
|
"requires": {
|
||||||
|
"ip": "^2.0.0",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"source-map": {
|
"source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
@@ -16771,9 +16684,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"streamsearch": {
|
"streamsearch": {
|
||||||
"version": "0.1.2",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
|
||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
@@ -17140,6 +17053,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
|
||||||
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
|
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"punycode": "^2.1.1"
|
"punycode": "^2.1.1"
|
||||||
}
|
}
|
||||||
@@ -17456,7 +17370,8 @@
|
|||||||
"webidl-conversions": {
|
"webidl-conversions": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz",
|
||||||
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w=="
|
"integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"whatwg-encoding": {
|
"whatwg-encoding": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"mongoose": "^6.0.12",
|
"mongoose": "^6.0.12",
|
||||||
"mongoose-sequence": "^5.3.1",
|
"mongoose-sequence": "^5.3.1",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.5-lts.1",
|
||||||
"rotating-file-stream": "^3.0.4",
|
"rotating-file-stream": "^3.0.4",
|
||||||
"swagger-ui-express": "4.3.0",
|
"swagger-ui-express": "4.3.0",
|
||||||
"unzipper": "^0.10.11",
|
"unzipper": "^0.10.11",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,10 @@ export default setProcessVariables().then(async () => {
|
|||||||
app.use(express.json({ limit: '100mb' }))
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
|
// Body parser is used for decoding the formdata on POST request.
|
||||||
|
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
await setupFolders()
|
await setupFolders()
|
||||||
await copySASjsCore()
|
await copySASjsCore()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
import { ExecutionController } from './internal'
|
||||||
import { ExecuteReturnJsonResponse } from '.'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
getUserAutoExec,
|
getUserAutoExec,
|
||||||
@@ -35,7 +34,7 @@ export class CodeController {
|
|||||||
public async executeCode(
|
public async executeCode(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body: ExecuteCodePayload
|
@Body() body: ExecuteCodePayload
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
return executeCode(request, body)
|
return executeCode(request, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,22 +50,15 @@ const executeCode = async (
|
|||||||
: await getUserAutoExec()
|
: await getUserAutoExec()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { result } = await new ExecutionController().executeProgram({
|
||||||
(await new ExecutionController().executeProgram({
|
program: code,
|
||||||
program: code,
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
vars: { ...req.query, _debug: 131 },
|
||||||
vars: { ...req.query, _debug: 131 },
|
otherArgs: { userAutoExec },
|
||||||
otherArgs: { userAutoExec },
|
runTime: runTime
|
||||||
returnJson: true,
|
})
|
||||||
runTime: runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
return {
|
return result
|
||||||
status: 'success',
|
|
||||||
_webout: webout as string,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ export interface ExecuteReturnRaw {
|
|||||||
result: string | Buffer
|
result: string | Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteReturnJson {
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
webout: string | Buffer
|
|
||||||
log?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecuteFileParams {
|
interface ExecuteFileParams {
|
||||||
programPath: string
|
programPath: string
|
||||||
preProgramVariables: PreProgramVars
|
preProgramVariables: PreProgramVars
|
||||||
@@ -68,10 +62,9 @@ export class ExecutionController {
|
|||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
|
||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController = getSessionController(runTime)
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
@@ -96,6 +89,7 @@ export class ExecutionController {
|
|||||||
vars,
|
vars,
|
||||||
session,
|
session,
|
||||||
weboutPath,
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
tokenFile,
|
tokenFile,
|
||||||
runTime,
|
runTime,
|
||||||
logPath,
|
logPath,
|
||||||
@@ -107,10 +101,7 @@ export class ExecutionController {
|
|||||||
? await readFile(headersPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
const fileResponse: boolean =
|
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||||
httpHeaders.hasOwnProperty('content-type') &&
|
|
||||||
!returnJson && // not a POST Request
|
|
||||||
!isDebugOn(vars) // Debug is not enabled
|
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse
|
||||||
@@ -121,19 +112,11 @@ export class ExecutionController {
|
|||||||
// it should be deleted by scheduleSessionDestroy
|
// it should be deleted by scheduleSessionDestroy
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
|
|
||||||
if (returnJson) {
|
|
||||||
return {
|
|
||||||
httpHeaders,
|
|
||||||
webout,
|
|
||||||
log: isDebugOn(vars) || session.crashed ? log : undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpHeaders,
|
httpHeaders,
|
||||||
result:
|
result:
|
||||||
isDebugOn(vars) || session.crashed
|
isDebugOn(vars) || session.crashed
|
||||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
? `${webout}\n${process.logsUUID}\n${log}`
|
||||||
: webout
|
: webout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,41 @@ import {
|
|||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
|
|
||||||
abstract class SessionController {
|
export class SessionController {
|
||||||
protected sessions: Session[] = []
|
protected sessions: Session[] = []
|
||||||
|
|
||||||
protected getReadySessions = (): Session[] =>
|
protected getReadySessions = (): Session[] =>
|
||||||
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||||
|
|
||||||
protected abstract createSession(): Promise<Session>
|
protected async createSession(): Promise<Session> {
|
||||||
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
|
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||||
|
|
||||||
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
// death time of session is 15 mins from creation
|
||||||
|
const deathTimeStamp = (
|
||||||
|
parseInt(creationTimeStamp) +
|
||||||
|
15 * 60 * 1000 -
|
||||||
|
1000
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
const session: Session = {
|
||||||
|
id: sessionId,
|
||||||
|
ready: true,
|
||||||
|
inUse: true,
|
||||||
|
consumed: false,
|
||||||
|
completed: false,
|
||||||
|
creationTimeStamp,
|
||||||
|
deathTimeStamp,
|
||||||
|
path: sessionFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
|
await createFile(headersPath, 'Content-type: text/plain')
|
||||||
|
|
||||||
|
this.sessions.push(session)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
public async getSession() {
|
public async getSession() {
|
||||||
const readySessions = this.getReadySessions()
|
const readySessions = this.getReadySessions()
|
||||||
@@ -101,12 +129,14 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
|
isWindows() ? '-nologo' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||||
isWindows() ? '-nologo' : ''
|
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
@@ -165,66 +195,17 @@ ${autoExecContent}`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JSSessionController extends SessionController {
|
|
||||||
protected async createSession(): Promise<Session> {
|
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
|
||||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
|
||||||
// death time of session is 15 mins from creation
|
|
||||||
const deathTimeStamp = (
|
|
||||||
parseInt(creationTimeStamp) +
|
|
||||||
15 * 60 * 1000 -
|
|
||||||
1000
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
const session: Session = {
|
|
||||||
id: sessionId,
|
|
||||||
ready: true,
|
|
||||||
inUse: true,
|
|
||||||
consumed: false,
|
|
||||||
completed: false,
|
|
||||||
creationTimeStamp,
|
|
||||||
deathTimeStamp,
|
|
||||||
path: sessionFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
|
||||||
await createFile(headersPath, 'Content-type: application/json')
|
|
||||||
|
|
||||||
this.sessions.push(session)
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSessionController = (
|
export const getSessionController = (
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
): SASSessionController | JSSessionController => {
|
): SessionController => {
|
||||||
if (runTime === RunTimeType.SAS) {
|
if (process.sessionController) return process.sessionController
|
||||||
return getSASSessionController()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runTime === RunTimeType.JS) {
|
process.sessionController =
|
||||||
return getJSSessionController()
|
runTime === RunTimeType.SAS
|
||||||
}
|
? new SASSessionController()
|
||||||
|
: new SessionController()
|
||||||
|
|
||||||
throw new Error('No Runtime is configured')
|
return process.sessionController
|
||||||
}
|
|
||||||
|
|
||||||
const getSASSessionController = (): SASSessionController => {
|
|
||||||
if (process.sasSessionController) return process.sasSessionController
|
|
||||||
|
|
||||||
process.sasSessionController = new SASSessionController()
|
|
||||||
|
|
||||||
return process.sasSessionController
|
|
||||||
}
|
|
||||||
|
|
||||||
const getJSSessionController = (): JSSessionController => {
|
|
||||||
if (process.jsSessionController) return process.jsSessionController
|
|
||||||
|
|
||||||
process.jsSessionController = new JSSessionController()
|
|
||||||
|
|
||||||
return process.jsSessionController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoExecContent = `
|
const autoExecContent = `
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const createJSProgram = async (
|
|||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
session: Session,
|
session: Session,
|
||||||
weboutPath: string,
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
tokenFile: string,
|
tokenFile: string,
|
||||||
otherArgs?: any
|
otherArgs?: any
|
||||||
) => {
|
) => {
|
||||||
@@ -23,15 +24,16 @@ let _webout = '';
|
|||||||
const weboutPath = '${
|
const weboutPath = '${
|
||||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||||
}';
|
}';
|
||||||
const _sasjs_tokenfile = '${
|
const _SASJS_TOKENFILE = '${
|
||||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
}';
|
}';
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _SASJS_WEBOUT_HEADERS = '${headersPath}';
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
const _SASJS_USERID = '${preProgramVariables?.userId}';
|
||||||
const _metaperson = _sasjs_displayname;
|
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||||
const _metauser = _sasjs_username;
|
const _METAPERSON = _SASJS_DISPLAYNAME;
|
||||||
const sasjsprocessmode = 'Stored Program';
|
const _METAUSER = _SASJS_USERNAME;
|
||||||
|
const SASJSPROCESSMODE = 'Stored Program';
|
||||||
`
|
`
|
||||||
|
|
||||||
const requiredModules = `const fs = require('fs')`
|
const requiredModules = `const fs = require('fs')`
|
||||||
@@ -55,14 +57,15 @@ if (_webout) {
|
|||||||
`
|
`
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
if (otherArgs?.filesNamesMap) {
|
if (otherArgs?.filesNamesMap) {
|
||||||
const uploadJSCode = await generateFileUploadJSCode(
|
const uploadJsCode = await generateFileUploadJSCode(
|
||||||
otherArgs.filesNamesMap,
|
otherArgs.filesNamesMap,
|
||||||
session.path
|
session.path
|
||||||
)
|
)
|
||||||
|
|
||||||
//If js code for the file is generated it will be appended to the top of jsCode
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
if (uploadJSCode.length > 0) {
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
program = `${uploadJSCode}\n` + program
|
if (uploadJsCode.length > 0) {
|
||||||
|
program = `${uploadJsCode}\n` + program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return requiredModules + program
|
return requiredModules + program
|
||||||
|
|||||||
68
api/src/controllers/internal/createPythonProgram.ts
Normal file
68
api/src/controllers/internal/createPythonProgram.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { isWindows } from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session } from '../../types'
|
||||||
|
import { generateFileUploadPythonCode } from '../../utils'
|
||||||
|
import { ExecutionVars } from './'
|
||||||
|
|
||||||
|
export const createPythonProgram = async (
|
||||||
|
program: string,
|
||||||
|
preProgramVariables: PreProgramVars,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
session: Session,
|
||||||
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
|
tokenFile: string,
|
||||||
|
otherArgs?: any
|
||||||
|
) => {
|
||||||
|
const varStatments = Object.keys(vars).reduce(
|
||||||
|
(computed: string, key: string) => `${computed}${key} = '${vars[key]}';\n`,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
const preProgramVarStatments = `
|
||||||
|
_SASJS_SESSION_PATH = '${
|
||||||
|
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
|
||||||
|
}';
|
||||||
|
_WEBOUT = '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
|
||||||
|
_SASJS_WEBOUT_HEADERS = '${headersPath}';
|
||||||
|
_SASJS_TOKENFILE = '${
|
||||||
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
|
}';
|
||||||
|
_SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||||
|
_SASJS_USERID = '${preProgramVariables?.userId}';
|
||||||
|
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||||
|
_METAPERSON = _SASJS_DISPLAYNAME;
|
||||||
|
_METAUSER = _SASJS_USERNAME;
|
||||||
|
SASJSPROCESSMODE = 'Stored Program';
|
||||||
|
`
|
||||||
|
|
||||||
|
const requiredModules = `import os`
|
||||||
|
|
||||||
|
program = `
|
||||||
|
# runtime vars
|
||||||
|
${varStatments}
|
||||||
|
|
||||||
|
# dynamic user-provided vars
|
||||||
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
# change working directory to session folder
|
||||||
|
os.chdir(_SASJS_SESSION_PATH)
|
||||||
|
|
||||||
|
# actual job code
|
||||||
|
${program}
|
||||||
|
|
||||||
|
`
|
||||||
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
|
if (otherArgs?.filesNamesMap) {
|
||||||
|
const uploadPythonCode = await generateFileUploadPythonCode(
|
||||||
|
otherArgs.filesNamesMap,
|
||||||
|
session.path
|
||||||
|
)
|
||||||
|
|
||||||
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
|
if (uploadPythonCode.length > 0) {
|
||||||
|
program = `${uploadPythonCode}\n` + program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredModules + program
|
||||||
|
}
|
||||||
68
api/src/controllers/internal/createRProgram.ts
Normal file
68
api/src/controllers/internal/createRProgram.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { isWindows } from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session } from '../../types'
|
||||||
|
import { generateFileUploadRCode } from '../../utils'
|
||||||
|
import { ExecutionVars } from '.'
|
||||||
|
|
||||||
|
export const createRProgram = async (
|
||||||
|
program: string,
|
||||||
|
preProgramVariables: PreProgramVars,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
session: Session,
|
||||||
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
|
tokenFile: string,
|
||||||
|
otherArgs?: any
|
||||||
|
) => {
|
||||||
|
const varStatments = Object.keys(vars).reduce(
|
||||||
|
(computed: string, key: string) => `${computed}.${key} <- '${vars[key]}'\n`,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
const preProgramVarStatments = `
|
||||||
|
._SASJS_SESSION_PATH <- '${
|
||||||
|
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
|
||||||
|
}';
|
||||||
|
._WEBOUT <- '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
|
||||||
|
._SASJS_WEBOUT_HEADERS <- '${headersPath}';
|
||||||
|
._SASJS_TOKENFILE <- '${
|
||||||
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
|
}';
|
||||||
|
._SASJS_USERNAME <- '${preProgramVariables?.username}';
|
||||||
|
._SASJS_USERID <- '${preProgramVariables?.userId}';
|
||||||
|
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
|
||||||
|
._METAPERSON <- ._SASJS_DISPLAYNAME;
|
||||||
|
._METAUSER <- ._SASJS_USERNAME;
|
||||||
|
SASJSPROCESSMODE <- 'Stored Program';
|
||||||
|
`
|
||||||
|
|
||||||
|
const requiredModules = ``
|
||||||
|
|
||||||
|
program = `
|
||||||
|
# runtime vars
|
||||||
|
${varStatments}
|
||||||
|
|
||||||
|
# dynamic user-provided vars
|
||||||
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
# change working directory to session folder
|
||||||
|
setwd(._SASJS_SESSION_PATH)
|
||||||
|
|
||||||
|
# actual job code
|
||||||
|
${program}
|
||||||
|
|
||||||
|
`
|
||||||
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
|
if (otherArgs?.filesNamesMap) {
|
||||||
|
const uploadRCode = await generateFileUploadRCode(
|
||||||
|
otherArgs.filesNamesMap,
|
||||||
|
session.path
|
||||||
|
)
|
||||||
|
|
||||||
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
|
if (uploadRCode.length > 0) {
|
||||||
|
program = `${uploadRCode}\n` + program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredModules + program
|
||||||
|
}
|
||||||
@@ -23,10 +23,14 @@ export const createSASProgram = async (
|
|||||||
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
||||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||||
|
%let _sasjs_webout_headers=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
||||||
%let _metaperson=&_sasjs_displayname;
|
%let _metaperson=&_sasjs_displayname;
|
||||||
%let _metauser=&_sasjs_username;
|
%let _metauser=&_sasjs_username;
|
||||||
|
|
||||||
|
/* the below is here for compatibility and will be removed in a future release */
|
||||||
|
%let sasjs_stpsrv_header_loc=&_sasjs_webout_headers;
|
||||||
|
|
||||||
%let sasjsprocessmode=Stored Program;
|
%let sasjsprocessmode=Stored Program;
|
||||||
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
|
||||||
|
|
||||||
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
||||||
%macro _sasjs_server_init();
|
%macro _sasjs_server_init();
|
||||||
@@ -34,6 +38,9 @@ export const createSASProgram = async (
|
|||||||
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
||||||
%mend;
|
%mend;
|
||||||
%_sasjs_server_init()
|
%_sasjs_server_init()
|
||||||
|
|
||||||
|
proc printto print="%sysfunc(getoption(log))";
|
||||||
|
run;
|
||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
@@ -60,7 +67,8 @@ ${program}`
|
|||||||
session.path
|
session.path
|
||||||
)
|
)
|
||||||
|
|
||||||
//If sas code for the file is generated it will be appended to the top of sasCode
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
if (uploadSasCode.length > 0) {
|
if (uploadSasCode.length > 0) {
|
||||||
program = `${uploadSasCode}` + program
|
program = `${uploadSasCode}` + program
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ export * from './Execution'
|
|||||||
export * from './FileUploadController'
|
export * from './FileUploadController'
|
||||||
export * from './createSASProgram'
|
export * from './createSASProgram'
|
||||||
export * from './createJSProgram'
|
export * from './createJSProgram'
|
||||||
|
export * from './createPythonProgram'
|
||||||
|
export * from './createRProgram'
|
||||||
export * from './processProgram'
|
export * from './processProgram'
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import { once } from 'stream'
|
|||||||
import { createFile, moveFile } from '@sasjs/utils'
|
import { createFile, moveFile } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session } from '../../types'
|
import { PreProgramVars, Session } from '../../types'
|
||||||
import { RunTimeType } from '../../utils'
|
import { RunTimeType } from '../../utils'
|
||||||
import { ExecutionVars, createSASProgram, createJSProgram } from './'
|
import {
|
||||||
|
ExecutionVars,
|
||||||
|
createSASProgram,
|
||||||
|
createJSProgram,
|
||||||
|
createPythonProgram,
|
||||||
|
createRProgram
|
||||||
|
} from './'
|
||||||
|
|
||||||
export const processProgram = async (
|
export const processProgram = async (
|
||||||
program: string,
|
program: string,
|
||||||
@@ -13,48 +19,13 @@ export const processProgram = async (
|
|||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
session: Session,
|
session: Session,
|
||||||
weboutPath: string,
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
tokenFile: string,
|
tokenFile: string,
|
||||||
runTime: RunTimeType,
|
runTime: RunTimeType,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
otherArgs?: any
|
otherArgs?: any
|
||||||
) => {
|
) => {
|
||||||
if (runTime === RunTimeType.JS) {
|
if (runTime === RunTimeType.SAS) {
|
||||||
program = await createJSProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
|
|
||||||
const codePath = path.join(session.path, 'code.js')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createFile(codePath, program)
|
|
||||||
|
|
||||||
// create a stream that will write to console outputs to log file
|
|
||||||
const writeStream = fs.createWriteStream(logPath)
|
|
||||||
|
|
||||||
// waiting for the open event so that we can have underlying file descriptor
|
|
||||||
await once(writeStream, 'open')
|
|
||||||
|
|
||||||
execFileSync(process.nodeLoc!, [codePath], {
|
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
|
||||||
})
|
|
||||||
|
|
||||||
// copy the code.js program to log and end write stream
|
|
||||||
writeStream.end(program)
|
|
||||||
|
|
||||||
session.completed = true
|
|
||||||
console.log('session completed', session)
|
|
||||||
} catch (err: any) {
|
|
||||||
session.completed = true
|
|
||||||
session.crashed = err.toString()
|
|
||||||
console.log('session crashed', session.id, session.crashed)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
program = await createSASProgram(
|
program = await createSASProgram(
|
||||||
program,
|
program,
|
||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
@@ -80,6 +51,82 @@ export const processProgram = async (
|
|||||||
while (!session.completed) {
|
while (!session.completed) {
|
||||||
await delay(50)
|
await delay(50)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let codePath: string
|
||||||
|
let executablePath: string
|
||||||
|
switch (runTime) {
|
||||||
|
case RunTimeType.JS:
|
||||||
|
program = await createJSProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.js')
|
||||||
|
executablePath = process.nodeLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
case RunTimeType.PY:
|
||||||
|
program = await createPythonProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.py')
|
||||||
|
executablePath = process.pythonLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
case RunTimeType.R:
|
||||||
|
program = await createRProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.r')
|
||||||
|
executablePath = process.rLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid runtime!')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createFile(codePath, program)
|
||||||
|
|
||||||
|
// create a stream that will write to console outputs to log file
|
||||||
|
const writeStream = fs.createWriteStream(logPath)
|
||||||
|
|
||||||
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
|
execFileSync(executablePath, [codePath], {
|
||||||
|
stdio: ['ignore', writeStream, writeStream]
|
||||||
|
})
|
||||||
|
|
||||||
|
// copy the code file to log and end write stream
|
||||||
|
writeStream.end(program)
|
||||||
|
|
||||||
|
session.completed = true
|
||||||
|
console.log('session completed', session)
|
||||||
|
} catch (err: any) {
|
||||||
|
session.completed = true
|
||||||
|
session.crashed = err.toString()
|
||||||
|
console.log('session crashed', session.id, session.crashed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
191
api/src/controllers/mock-sas9.ts
Normal file
191
api/src/controllers/mock-sas9.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { readFile } from '@sasjs/utils'
|
||||||
|
import express from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import { Request, Post, Get } from 'tsoa'
|
||||||
|
|
||||||
|
export interface Sas9Response {
|
||||||
|
content: string
|
||||||
|
redirect?: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockFileRead {
|
||||||
|
content: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockSas9Controller {
|
||||||
|
private loggedIn: string | undefined
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess')
|
||||||
|
public async sasStoredProcess(): Promise<Sas9Response> {
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'sas-stored-process'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASStoredProcess/do/')
|
||||||
|
public async sasStoredProcessDo(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPublicAccount()) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/Login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let program = req.query._program?.toString() || ''
|
||||||
|
program = program.replace('/', '')
|
||||||
|
|
||||||
|
const content = await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
...program.split('/')
|
||||||
|
])
|
||||||
|
|
||||||
|
if (content.error) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = parseJsonIfValid(content.content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: parsedContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASLogon/login')
|
||||||
|
public async loginGet(): Promise<Sas9Response> {
|
||||||
|
if (this.loggedIn) {
|
||||||
|
if (this.isPublicAccount()) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASStoredProcess/Logoff?publicDenied=true'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'logged-in'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'login'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASLogon/login')
|
||||||
|
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||||
|
this.loggedIn = req.body.username
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'logged-in'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASLogon/logout')
|
||||||
|
public async logout(req: express.Request): Promise<Sas9Response> {
|
||||||
|
this.loggedIn = undefined
|
||||||
|
|
||||||
|
if (req.query.publicDenied === 'true') {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'public-access-denied'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'logged-out'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess/Logoff') //publicDenied=true
|
||||||
|
public async logoff(req: express.Request): Promise<Sas9Response> {
|
||||||
|
const params = req.query.publicDenied
|
||||||
|
? `?publicDenied=${req.query.publicDenied}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/logout' + params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If JSON is valid it will be parsed otherwise will return text unaltered
|
||||||
|
* @param content string to be parsed
|
||||||
|
* @returns JSON or string
|
||||||
|
*/
|
||||||
|
const parseJsonIfValid = (content: string) => {
|
||||||
|
let fileContent = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileContent = JSON.parse(content)
|
||||||
|
} catch (err: any) {
|
||||||
|
fileContent = content
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileContent
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMockResponseFromFile = async (
|
||||||
|
filePath: string[]
|
||||||
|
): Promise<MockFileRead> => {
|
||||||
|
const filePathParsed = path.join(...filePath)
|
||||||
|
let error: boolean = false
|
||||||
|
|
||||||
|
let file = await readFile(filePathParsed).catch((err: any) => {
|
||||||
|
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
|
||||||
|
console.error(errMsg)
|
||||||
|
|
||||||
|
error = true
|
||||||
|
|
||||||
|
return errMsg
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: file,
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,16 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import {
|
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
||||||
Request,
|
import { ExecutionController, ExecutionVars } from './internal'
|
||||||
Security,
|
|
||||||
Route,
|
|
||||||
Tags,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Get,
|
|
||||||
Query,
|
|
||||||
Example
|
|
||||||
} from 'tsoa'
|
|
||||||
import {
|
|
||||||
ExecuteReturnJson,
|
|
||||||
ExecuteReturnRaw,
|
|
||||||
ExecutionController,
|
|
||||||
ExecutionVars
|
|
||||||
} from './internal'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
|
||||||
LogLine,
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
parseLogToArray,
|
|
||||||
getRunTimeAndFilePath
|
getRunTimeAndFilePath
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecutePostRequestPayload {
|
||||||
/**
|
/**
|
||||||
* Location of SAS program
|
* Location of SAS program
|
||||||
* @example "/Public/somefolder/some.file"
|
* @example "/Public/somefolder/some.file"
|
||||||
@@ -35,17 +18,6 @@ interface ExecuteReturnJsonPayload {
|
|||||||
_program?: string
|
_program?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRecordOfAny {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
export interface ExecuteReturnJsonResponse {
|
|
||||||
status: string
|
|
||||||
_webout: string | IRecordOfAny
|
|
||||||
log: LogLine[]
|
|
||||||
message?: string
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/stp')
|
@Route('SASjsApi/stp')
|
||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
@@ -62,11 +34,12 @@ export class STPController {
|
|||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Get('/execute')
|
@Get('/execute')
|
||||||
public async executeReturnRaw(
|
public async executeGetRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Query() _program: string
|
@Query() _program: string
|
||||||
): Promise<string | Buffer> {
|
): Promise<string | Buffer> {
|
||||||
return executeReturnRaw(request, _program)
|
const vars = request.query as ExecutionVars
|
||||||
|
return execute(request, _program, vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,50 +60,42 @@ export class STPController {
|
|||||||
* @param _program Location of SAS or JS code
|
* @param _program Location of SAS or JS code
|
||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Example<ExecuteReturnJsonResponse>({
|
|
||||||
status: 'success',
|
|
||||||
_webout: 'webout content',
|
|
||||||
log: [],
|
|
||||||
httpHeaders: {
|
|
||||||
'Content-type': 'application/zip',
|
|
||||||
'Cache-Control': 'public, max-age=1000'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executeReturnJson(
|
public async executePostRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body?: ExecuteReturnJsonPayload,
|
@Body() body?: ExecutePostRequestPayload,
|
||||||
@Query() _program?: string
|
@Query() _program?: string
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
const program = _program ?? body?._program
|
const program = _program ?? body?._program
|
||||||
return executeReturnJson(request, program!)
|
const vars = { ...request.query, ...request.body }
|
||||||
|
const filesNamesMap = request.files?.length
|
||||||
|
? makeFilesNamesMap(request.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||||
|
|
||||||
|
return execute(request, program!, vars, otherArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnRaw = async (
|
const execute = async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string
|
_program: string,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
otherArgs?: any
|
||||||
): Promise<string | Buffer> => {
|
): Promise<string | Buffer> => {
|
||||||
const query = req.query as ExecutionVars
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
||||||
|
|
||||||
const { result, httpHeaders } =
|
const { result, httpHeaders } = await new ExecutionController().executeFile(
|
||||||
(await new ExecutionController().executeFile({
|
{
|
||||||
programPath: codePath,
|
programPath: codePath,
|
||||||
|
runTime,
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
vars: query,
|
vars,
|
||||||
runTime
|
otherArgs,
|
||||||
})) as ExecuteReturnRaw
|
session: req.sasjsSession
|
||||||
|
}
|
||||||
// Should over-ride response header for debug
|
)
|
||||||
// on GET request to see entire log rendering on browser.
|
|
||||||
if (isDebugOn(query)) {
|
|
||||||
httpHeaders['content-type'] = 'text/plain'
|
|
||||||
}
|
|
||||||
|
|
||||||
req.res?.set(httpHeaders)
|
|
||||||
|
|
||||||
if (result instanceof Buffer) {
|
if (result instanceof Buffer) {
|
||||||
;(req as any).sasHeaders = httpHeaders
|
;(req as any).sasHeaders = httpHeaders
|
||||||
@@ -146,48 +111,3 @@ const executeReturnRaw = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnJson = async (
|
|
||||||
req: express.Request,
|
|
||||||
_program: string
|
|
||||||
): Promise<ExecuteReturnJsonResponse> => {
|
|
||||||
const filesNamesMap = req.files?.length
|
|
||||||
? makeFilesNamesMap(req.files as MulterFile[])
|
|
||||||
: null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
|
||||||
|
|
||||||
const { webout, log, httpHeaders } =
|
|
||||||
(await new ExecutionController().executeFile({
|
|
||||||
programPath: codePath,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars: { ...req.query, ...req.body },
|
|
||||||
otherArgs: { filesNamesMap: filesNamesMap },
|
|
||||||
returnJson: true,
|
|
||||||
session: req.sasjsSession,
|
|
||||||
runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
let weboutRes: string | IRecordOfAny = webout
|
|
||||||
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
|
||||||
try {
|
|
||||||
weboutRes = JSON.parse(webout as string)
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
_webout: weboutRes,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'Job execution failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
||||||
import {
|
import {
|
||||||
SASSessionController,
|
SessionController,
|
||||||
JSSessionController
|
SASSessionController
|
||||||
} from '../../../controllers/internal'
|
} from '../../../controllers/internal'
|
||||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
||||||
import { Session } from '../../../types'
|
import { Session } from '../../../types'
|
||||||
@@ -39,14 +39,17 @@ const user = {
|
|||||||
|
|
||||||
const sampleSasProgram = '%put hello world!;'
|
const sampleSasProgram = '%put hello world!;'
|
||||||
const sampleJsProgram = `console.log('hello world!/')`
|
const sampleJsProgram = `console.log('hello world!/')`
|
||||||
|
const samplePyProgram = `print('hello world!/')`
|
||||||
|
|
||||||
const filesFolder = getFilesFolder()
|
const filesFolder = getFilesFolder()
|
||||||
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
let accessToken: string
|
||||||
|
|
||||||
describe('stp', () => {
|
describe('stp', () => {
|
||||||
let app: Express
|
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let accessToken: string
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const permissionController = new PermissionController()
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
@@ -72,8 +75,6 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('execute', () => {
|
describe('execute', () => {
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
|
||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
describe('with runtime js', () => {
|
describe('with runtime js', () => {
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
@@ -93,41 +94,45 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
it('should execute js program when both js and sas program are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.JS, RunTimeType.SAS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.JS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when js program is not present but sas program exists', async () => {
|
it('should throw error when js program is not present but sas program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
})
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime py', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
beforeAll(() => {
|
||||||
.expect(400)
|
process.runTimes = [RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when python, js and sas programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when py program is not present but js or sas program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,41 +152,11 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs are present', async () => {
|
it('should execute sas program when both sas and js programs are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when sas program do not exit but js exists', async () => {
|
it('should throw error when sas program do not exit but js exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(400)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -201,63 +176,51 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
it('should execute js program when both js and sas program are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.JS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when js program is not present but sas program exists', async () => {
|
it('should execute sas program when js program is not present but sas program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime py and sas', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
beforeAll(() => {
|
||||||
.auth(accessToken, { type: 'bearer' })
|
process.runTimes = [RunTimeType.PY, RunTimeType.SAS]
|
||||||
.send()
|
})
|
||||||
.expect(400)
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both python and sas program are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when python program is not present but sas program exists', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -277,76 +240,220 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs exist', async () => {
|
it('should execute sas program when both sas and js programs exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.SAS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when sas program is not present but js program exists', async () => {
|
it('should execute js program when sas program is not present but js program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime sas and py', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
beforeAll(() => {
|
||||||
.auth(accessToken, { type: 'bearer' })
|
process.runTimes = [RunTimeType.SAS, RunTimeType.PY]
|
||||||
.send()
|
})
|
||||||
.expect(400)
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when both sas and python programs exist', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when sas program is not present but python program exists', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when both sas and python programs do not exist', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime sas, js and py', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.SAS, RunTimeType.JS, RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when it exists, no matter js and python programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when sas program is absent but js and python programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.JS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.JS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both sas and js programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime js, sas and py', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when it exists, no matter sas and python programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.JS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when js program is absent but sas and python programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both sas and js programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime py, sas and js', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when it exists, no matter sas and js programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when python program is absent but sas and js programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when both sas and python programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const generateSaveTokenAndCreateUser = async (
|
const makeRequestAndAssert = async (
|
||||||
someUser: any
|
programTypes: RunTimeType[],
|
||||||
): Promise<string> => {
|
expectedStatusCode: number,
|
||||||
const userController = new UserController()
|
expectedRuntime?: RunTimeType
|
||||||
const dbUser = await userController.createUser(someUser)
|
) => {
|
||||||
|
const programPath = path.join(testFilesFolder, 'program')
|
||||||
|
for (const programType of programTypes) {
|
||||||
|
if (programType === RunTimeType.JS)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.js`),
|
||||||
|
sampleJsProgram
|
||||||
|
)
|
||||||
|
else if (programType === RunTimeType.PY)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.py`),
|
||||||
|
samplePyProgram
|
||||||
|
)
|
||||||
|
else if (programType === RunTimeType.SAS)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.sas`),
|
||||||
|
sampleSasProgram
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return generateAndSaveToken(dbUser.id)
|
await request(app)
|
||||||
|
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(expectedStatusCode)
|
||||||
|
|
||||||
|
if (expectedRuntime)
|
||||||
|
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expectedRuntime,
|
||||||
|
expect.anything(),
|
||||||
|
undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
const generateAndSaveToken = async (userId: number) => {
|
||||||
@@ -364,7 +471,7 @@ const setupMocks = async () => {
|
|||||||
.mockImplementation(mockedGetSession)
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(JSSessionController.prototype, 'getSession')
|
.spyOn(SASSessionController.prototype, 'getSession')
|
||||||
.mockImplementation(mockedGetSession)
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnRaw(req, query._program)
|
const response = await controller.executeGetRequest(req, query._program)
|
||||||
|
|
||||||
if (response instanceof Buffer) {
|
if (response instanceof Buffer) {
|
||||||
res.writeHead(200, (req as any).sasHeaders)
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
@@ -42,7 +42,7 @@ stpRouter.post(
|
|||||||
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnJson(
|
const response = await controller.executePostRequest(
|
||||||
req,
|
req,
|
||||||
req.body,
|
req.body,
|
||||||
req.query?._program as string
|
req.query?._program as string
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import sas9WebRouter from './sas9-web'
|
||||||
|
import sasViyaWebRouter from './sasviya-web'
|
||||||
import webRouter from './web'
|
import webRouter from './web'
|
||||||
|
import { MOCK_SERVERTYPEType } from '../../utils'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.use('/', webRouter)
|
const { MOCK_SERVERTYPE } = process.env
|
||||||
|
|
||||||
|
switch (MOCK_SERVERTYPE) {
|
||||||
|
case MOCK_SERVERTYPEType.SAS9: {
|
||||||
|
router.use('/', sas9WebRouter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case MOCK_SERVERTYPEType.SASVIYA: {
|
||||||
|
router.use('/', sasViyaWebRouter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
router.use('/', webRouter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
118
api/src/routes/web/sas9-web.ts
Normal file
118
api/src/routes/web/sas9-web.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { WebController } from '../../controllers'
|
||||||
|
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||||
|
|
||||||
|
const sas9WebRouter = express.Router()
|
||||||
|
const webController = new WebController()
|
||||||
|
// Mock controller must be singleton because it keeps the states
|
||||||
|
// for example `isLoggedIn` and potentially more in future mocks
|
||||||
|
const controller = new MockSas9Controller()
|
||||||
|
|
||||||
|
sas9WebRouter.get('/', async (req, res) => {
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await webController.home()
|
||||||
|
} catch (_) {
|
||||||
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
|
} finally {
|
||||||
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.send(injectedContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcess()
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcessDo(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
||||||
|
const response = await controller.loginGet()
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
|
||||||
|
const response = await controller.loginPost(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASLogon/logout', async (req, res) => {
|
||||||
|
const response = await controller.logout(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASStoredProcess/Logoff', async (req, res) => {
|
||||||
|
const response = await controller.logoff(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default sas9WebRouter
|
||||||
32
api/src/routes/web/sasviya-web.ts
Normal file
32
api/src/routes/web/sasviya-web.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { WebController } from '../../controllers/web'
|
||||||
|
|
||||||
|
const sasViyaWebRouter = express.Router()
|
||||||
|
const controller = new WebController()
|
||||||
|
|
||||||
|
sasViyaWebRouter.get('/', async (req, res) => {
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await controller.home()
|
||||||
|
} catch (_) {
|
||||||
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
|
} finally {
|
||||||
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.send(injectedContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sasViyaWebRouter.post('/SASJobExecution/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.send({ test: 'test' })
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default sasViyaWebRouter
|
||||||
6
api/src/types/system/process.d.ts
vendored
6
api/src/types/system/process.d.ts
vendored
@@ -2,10 +2,12 @@ declare namespace NodeJS {
|
|||||||
export interface Process {
|
export interface Process {
|
||||||
sasLoc?: string
|
sasLoc?: string
|
||||||
nodeLoc?: string
|
nodeLoc?: string
|
||||||
|
pythonLoc?: string
|
||||||
|
rLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
logsLoc: string
|
logsLoc: string
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
logsUUID: string
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
|
|||||||
import { RunTimeType } from './verifyEnvVariables'
|
import { RunTimeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, NODE_PATH } = process.env
|
const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env
|
||||||
|
|
||||||
let sasLoc, nodeLoc
|
let sasLoc, nodeLoc, pythonLoc, rLoc
|
||||||
|
|
||||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
@@ -16,7 +16,15 @@ export const getDesktopFields = async () => {
|
|||||||
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sasLoc, nodeLoc }
|
if (process.runTimes.includes(RunTimeType.PY)) {
|
||||||
|
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.runTimes.includes(RunTimeType.R)) {
|
||||||
|
rLoc = R_PATH ?? (await getRLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sasLoc, nodeLoc, pythonLoc, rLoc }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriveLocation = async (): Promise<string> => {
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
@@ -91,3 +99,47 @@ const getNodeLocation = async (): Promise<string> => {
|
|||||||
|
|
||||||
return targetName
|
return targetName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPythonLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to Python executable is required.'
|
||||||
|
|
||||||
|
if (!(await fileExists(filePath))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows() ? 'C:\\Python' : '/usr/bin/python'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter full path to a Python executable: ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to R executable is required.'
|
||||||
|
|
||||||
|
if (!(await fileExists(filePath))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows() ? 'C:\\Rscript' : '/usr/bin/Rscript'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter full path to a R executable: ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RunTimeType } from '.'
|
|||||||
|
|
||||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||||
const ext = path.extname(programPath)
|
const ext = path.extname(programPath)
|
||||||
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
// If programPath (_program) is provided with a ".sas", ".js", ".py" or ".r" extension
|
||||||
// we should use that extension to determine the appropriate runTime
|
// we should use that extension to determine the appropriate runTime
|
||||||
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
||||||
const runTime = ext.slice(1)
|
const runTime = ext.slice(1)
|
||||||
|
|||||||
@@ -28,11 +28,15 @@ export const setProcessVariables = async () => {
|
|||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
process.sasLoc = process.env.SAS_PATH
|
process.sasLoc = process.env.SAS_PATH
|
||||||
process.nodeLoc = process.env.NODE_PATH
|
process.nodeLoc = process.env.NODE_PATH
|
||||||
|
process.pythonLoc = process.env.PYTHON_PATH
|
||||||
|
process.rLoc = process.env.R_PATH
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
process.sasLoc = sasLoc
|
||||||
process.nodeLoc = nodeLoc
|
process.nodeLoc = nodeLoc
|
||||||
|
process.pythonLoc = pythonLoc
|
||||||
|
process.rLoc = rLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SASJS_ROOT } = process.env
|
const { SASJS_ROOT } = process.env
|
||||||
@@ -48,6 +52,8 @@ export const setProcessVariables = async () => {
|
|||||||
await createFolder(absLogsPath)
|
await createFolder(absLogsPath)
|
||||||
process.logsLoc = getRealPath(absLogsPath)
|
process.logsLoc = getRealPath(absLogsPath)
|
||||||
|
|
||||||
|
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
console.log('sasLogs: ', process.logsLoc)
|
console.log('sasLogs: ', process.logsLoc)
|
||||||
|
|||||||
@@ -126,9 +126,61 @@ export const generateFileUploadJSCode = async (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (fileCount) {
|
uploadCode += `\nconst _WEBIN_FILE_COUNT = ${fileCount}`
|
||||||
uploadCode = `\nconst _WEBIN_FILE_COUNT = ${fileCount}` + uploadCode
|
|
||||||
}
|
return uploadCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the python code that references uploaded files in the concurrent request
|
||||||
|
* @param filesNamesMap object that maps hashed file names and original file names
|
||||||
|
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
|
||||||
|
* @returns generated python code
|
||||||
|
*/
|
||||||
|
export const generateFileUploadPythonCode = async (
|
||||||
|
filesNamesMap: FilenamesMap,
|
||||||
|
sessionFolder: string
|
||||||
|
) => {
|
||||||
|
let uploadCode = ''
|
||||||
|
let fileCount = 0
|
||||||
|
|
||||||
|
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
|
||||||
|
sessionFolderList.forEach(async (fileName) => {
|
||||||
|
if (fileName.includes('req_file')) {
|
||||||
|
fileCount++
|
||||||
|
uploadCode += `\n_WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||||
|
uploadCode += `\n_WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadCode += `\n_WEBIN_FILE_COUNT = ${fileCount}`
|
||||||
|
|
||||||
|
return uploadCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the R code that references uploaded files in the concurrent request
|
||||||
|
* @param filesNamesMap object that maps hashed file names and original file names
|
||||||
|
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
|
||||||
|
* @returns generated python code
|
||||||
|
*/
|
||||||
|
export const generateFileUploadRCode = async (
|
||||||
|
filesNamesMap: FilenamesMap,
|
||||||
|
sessionFolder: string
|
||||||
|
) => {
|
||||||
|
let uploadCode = ''
|
||||||
|
let fileCount = 0
|
||||||
|
|
||||||
|
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
|
||||||
|
sessionFolderList.forEach(async (fileName) => {
|
||||||
|
if (fileName.includes('req_file')) {
|
||||||
|
fileCount++
|
||||||
|
uploadCode += `\n._WEBIN_FILENAME${fileCount} <- '${filesNamesMap[fileName].originalName}'`
|
||||||
|
uploadCode += `\n._WEBIN_NAME${fileCount} <- '${filesNamesMap[fileName].fieldName}'`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadCode += `\n._WEBIN_FILE_COUNT <- ${fileCount}`
|
||||||
|
|
||||||
return uploadCode
|
return uploadCode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
export enum MOCK_SERVERTYPEType {
|
||||||
|
SAS9 = 'sas9',
|
||||||
|
SASVIYA = 'sasviya'
|
||||||
|
}
|
||||||
|
|
||||||
export enum ModeType {
|
export enum ModeType {
|
||||||
Server = 'server',
|
Server = 'server',
|
||||||
Desktop = 'desktop'
|
Desktop = 'desktop'
|
||||||
@@ -28,7 +33,9 @@ export enum LOG_FORMAT_MORGANType {
|
|||||||
|
|
||||||
export enum RunTimeType {
|
export enum RunTimeType {
|
||||||
SAS = 'sas',
|
SAS = 'sas',
|
||||||
JS = 'js'
|
JS = 'js',
|
||||||
|
PY = 'py',
|
||||||
|
R = 'r'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReturnCode {
|
export enum ReturnCode {
|
||||||
@@ -39,6 +46,8 @@ export enum ReturnCode {
|
|||||||
export const verifyEnvVariables = (): ReturnCode => {
|
export const verifyEnvVariables = (): ReturnCode => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
|
||||||
|
errors.push(...verifyMOCK_SERVERTYPE())
|
||||||
|
|
||||||
errors.push(...verifyMODE())
|
errors.push(...verifyMODE())
|
||||||
|
|
||||||
errors.push(...verifyPROTOCOL())
|
errors.push(...verifyPROTOCOL())
|
||||||
@@ -65,6 +74,23 @@ export const verifyEnvVariables = (): ReturnCode => {
|
|||||||
return ReturnCode.Success
|
return ReturnCode.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyMOCK_SERVERTYPE = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { MOCK_SERVERTYPE } = process.env
|
||||||
|
|
||||||
|
if (MOCK_SERVERTYPE) {
|
||||||
|
const modeTypes = Object.values(MOCK_SERVERTYPEType)
|
||||||
|
if (!modeTypes.includes(MOCK_SERVERTYPE as MOCK_SERVERTYPEType))
|
||||||
|
errors.push(
|
||||||
|
`- MOCK_SERVERTYPE '${MOCK_SERVERTYPE}'\n - valid options ${modeTypes}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
process.env.MOCK_SERVERTYPE = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
const verifyMODE = (): string[] => {
|
const verifyMODE = (): string[] => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
@@ -228,7 +254,8 @@ const verifyRUN_TIMES = (): string[] => {
|
|||||||
|
|
||||||
const verifyExecutablePaths = () => {
|
const verifyExecutablePaths = () => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, MODE } = process.env
|
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
|
||||||
|
process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
const runTimes = RUN_TIMES?.split(',')
|
const runTimes = RUN_TIMES?.split(',')
|
||||||
@@ -240,6 +267,14 @@ const verifyExecutablePaths = () => {
|
|||||||
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
|
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
|
||||||
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
|
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) {
|
||||||
|
errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runTimes?.includes(RunTimeType.R) && !R_PATH) {
|
||||||
|
errors.push(`- R_PATH is required for ${RunTimeType.R} run time`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
331
web/package-lock.json
generated
331
web/package-lock.json
generated
@@ -2284,6 +2284,58 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/set-array": "^1.0.1",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/set-array": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/source-map": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.0",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||||
|
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
|
||||||
|
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mui/core": {
|
"node_modules/@mui/core": {
|
||||||
"version": "5.0.0-alpha.54",
|
"version": "5.0.0-alpha.54",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
|
||||||
@@ -3937,11 +3989,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "7.4.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
|
||||||
"dev": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -6522,18 +6572,6 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/espree/node_modules/acorn": {
|
|
||||||
"version": "8.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
|
||||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"acorn": "bin/acorn"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/espree/node_modules/eslint-visitor-keys": {
|
"node_modules/espree/node_modules/eslint-visitor-keys": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
|
||||||
@@ -7197,20 +7235,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html-minifier-terser/node_modules/acorn": {
|
|
||||||
"version": "8.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
|
||||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"acorn": "bin/acorn"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/html-minifier-terser/node_modules/commander": {
|
"node_modules/html-minifier-terser/node_modules/commander": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
@@ -7220,46 +7244,6 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html-minifier-terser/node_modules/source-map": {
|
|
||||||
"version": "0.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
|
||||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/html-minifier-terser/node_modules/terser": {
|
|
||||||
"version": "5.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
|
||||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^2.20.0",
|
|
||||||
"source-map": "~0.7.2",
|
|
||||||
"source-map-support": "~0.5.20"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"terser": "bin/terser"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"acorn": "^8.5.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"acorn": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/html-webpack-plugin": {
|
"node_modules/html-webpack-plugin": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz",
|
||||||
@@ -10287,6 +10271,28 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/terser": {
|
||||||
|
"version": "5.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||||
|
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/source-map": "^0.3.2",
|
||||||
|
"acorn": "^8.5.0",
|
||||||
|
"commander": "^2.20.0",
|
||||||
|
"source-map-support": "~0.5.20"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"terser": "bin/terser"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/terser/node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -11124,17 +11130,6 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/acorn": {
|
|
||||||
"version": "8.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
|
||||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
|
||||||
"bin": {
|
|
||||||
"acorn": "bin/acorn"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/acorn-import-assertions": {
|
"node_modules/webpack/node_modules/acorn-import-assertions": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
|
||||||
@@ -11143,11 +11138,6 @@
|
|||||||
"acorn": "^8"
|
"acorn": "^8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/has-flag": {
|
"node_modules/webpack/node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -11216,30 +11206,6 @@
|
|||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/terser": {
|
|
||||||
"version": "5.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
|
||||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
|
||||||
"dependencies": {
|
|
||||||
"commander": "^2.20.0",
|
|
||||||
"source-map": "~0.7.2",
|
|
||||||
"source-map-support": "~0.5.20"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"terser": "bin/terser"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"acorn": "^8.5.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"acorn": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/terser-webpack-plugin": {
|
"node_modules/webpack/node_modules/terser-webpack-plugin": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",
|
||||||
@@ -11273,14 +11239,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack/node_modules/terser/node_modules/source-map": {
|
|
||||||
"version": "0.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
|
||||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webpack/node_modules/webpack-sources": {
|
"node_modules/webpack/node_modules/webpack-sources": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||||
@@ -12914,6 +12872,49 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@jridgewell/gen-mapping": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
|
||||||
|
"requires": {
|
||||||
|
"@jridgewell/set-array": "^1.0.1",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@jridgewell/resolve-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
|
||||||
|
},
|
||||||
|
"@jridgewell/set-array": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
|
||||||
|
},
|
||||||
|
"@jridgewell/source-map": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
|
||||||
|
"requires": {
|
||||||
|
"@jridgewell/gen-mapping": "^0.3.0",
|
||||||
|
"@jridgewell/trace-mapping": "^0.3.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||||
|
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||||
|
},
|
||||||
|
"@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
|
||||||
|
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
|
||||||
|
"requires": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@mui/core": {
|
"@mui/core": {
|
||||||
"version": "5.0.0-alpha.54",
|
"version": "5.0.0-alpha.54",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core/-/core-5.0.0-alpha.54.tgz",
|
||||||
@@ -14110,11 +14111,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"acorn": {
|
"acorn": {
|
||||||
"version": "7.4.1",
|
"version": "8.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
|
||||||
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
|
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w=="
|
||||||
"dev": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"acorn-jsx": {
|
"acorn-jsx": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
@@ -16061,12 +16060,6 @@
|
|||||||
"eslint-visitor-keys": "^3.3.0"
|
"eslint-visitor-keys": "^3.3.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": {
|
|
||||||
"version": "8.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
|
||||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"eslint-visitor-keys": {
|
"eslint-visitor-keys": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
|
||||||
@@ -16585,44 +16578,11 @@
|
|||||||
"terser": "^5.10.0"
|
"terser": "^5.10.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": {
|
|
||||||
"version": "8.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
|
||||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
|
||||||
"source-map": {
|
|
||||||
"version": "0.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
|
||||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"terser": {
|
|
||||||
"version": "5.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
|
||||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"commander": "^2.20.0",
|
|
||||||
"source-map": "~0.7.2",
|
|
||||||
"source-map-support": "~0.5.20"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -18903,6 +18863,24 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
|
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="
|
||||||
},
|
},
|
||||||
|
"terser": {
|
||||||
|
"version": "5.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
|
||||||
|
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
|
||||||
|
"requires": {
|
||||||
|
"@jridgewell/source-map": "^0.3.2",
|
||||||
|
"acorn": "^8.5.0",
|
||||||
|
"commander": "^2.20.0",
|
||||||
|
"source-map-support": "~0.5.20"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"text-table": {
|
"text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -19259,22 +19237,12 @@
|
|||||||
"webpack-sources": "^3.2.2"
|
"webpack-sources": "^3.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": {
|
|
||||||
"version": "8.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
|
||||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ=="
|
|
||||||
},
|
|
||||||
"acorn-import-assertions": {
|
"acorn-import-assertions": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
|
||||||
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
|
"integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
|
||||||
},
|
|
||||||
"has-flag": {
|
"has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -19321,23 +19289,6 @@
|
|||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"terser": {
|
|
||||||
"version": "5.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
|
|
||||||
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
|
|
||||||
"requires": {
|
|
||||||
"commander": "^2.20.0",
|
|
||||||
"source-map": "~0.7.2",
|
|
||||||
"source-map-support": "~0.5.20"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"source-map": {
|
|
||||||
"version": "0.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
|
|
||||||
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"terser-webpack-plugin": {
|
"terser-webpack-plugin": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const FilePathInputModal = ({
|
|||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/
|
const specialChars = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>?~]/
|
||||||
const fileExtension = /\.(exe|sh|htaccess)$/i
|
const fileExtension = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
if (specialChars.test(value)) {
|
if (specialChars.test(value)) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const NameInputModal = ({
|
|||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
||||||
const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/
|
const fileNameRegex = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>/?~]/
|
||||||
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ const TreeViewNode = ({
|
|||||||
mouseY: number
|
mouseY: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
const launchProgram = () => {
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchProgramWithDebug = () => {
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
window.open(
|
||||||
|
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handleContextMenu = (event: React.MouseEvent) => {
|
const handleContextMenu = (event: React.MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -224,8 +236,8 @@ const TreeViewNode = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{node.isFolder && (
|
{node.isFolder ? (
|
||||||
<div>
|
<>
|
||||||
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
|
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!node.relativePath}
|
disabled={!node.relativePath}
|
||||||
@@ -233,14 +245,21 @@ const TreeViewNode = ({
|
|||||||
>
|
>
|
||||||
Add File
|
Add File
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={launchProgram}>Launch</MenuItem>
|
||||||
|
<MenuItem onClick={launchProgramWithDebug}>
|
||||||
|
Launch and Debug
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!!node.relativePath && (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={handleRenameItemClick}>Rename</MenuItem>
|
||||||
|
<MenuItem onClick={handleDeleteItemClick}>Delete</MenuItem>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem disabled={!node.relativePath} onClick={handleRenameItemClick}>
|
|
||||||
Rename
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
|
||||||
Delete
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Permission from './permission'
|
|||||||
import Profile from './profile'
|
import Profile from './profile'
|
||||||
|
|
||||||
import { AppContext, ModeType } from '../../context/appContext'
|
import { AppContext, ModeType } from '../../context/appContext'
|
||||||
|
import PermissionsContextProvider from '../../context/permissionsContext'
|
||||||
|
|
||||||
const StyledTab = styled(Tab)({
|
const StyledTab = styled(Tab)({
|
||||||
background: 'black',
|
background: 'black',
|
||||||
@@ -64,7 +65,9 @@ const Settings = () => {
|
|||||||
<Profile />
|
<Profile />
|
||||||
</StyledTabpanel>
|
</StyledTabpanel>
|
||||||
<StyledTabpanel value="permission">
|
<StyledTabpanel value="permission">
|
||||||
<Permission />
|
<PermissionsContextProvider>
|
||||||
|
<Permission />
|
||||||
|
</PermissionsContextProvider>
|
||||||
</StyledTabpanel>
|
</StyledTabpanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { IconButton, Tooltip } from '@mui/material'
|
||||||
|
import { Add } from '@mui/icons-material'
|
||||||
|
import { RegisterPermissionPayload } from '../../../../utils/types'
|
||||||
|
import AddPermissionModal from './addPermissionModal'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
openModal: boolean
|
||||||
|
setOpenModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
addPermission: (
|
||||||
|
permissionsToAdd: RegisterPermissionPayload[],
|
||||||
|
permissionType: string,
|
||||||
|
principalType: string,
|
||||||
|
principal: string,
|
||||||
|
permissionSetting: string
|
||||||
|
) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddPermission = ({ openModal, setOpenModal, addPermission }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
sx={{ marginLeft: 'auto' }}
|
||||||
|
title="Add Permission"
|
||||||
|
placement="bottom-end"
|
||||||
|
>
|
||||||
|
<IconButton onClick={() => setOpenModal(true)}>
|
||||||
|
<Add />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<AddPermissionModal
|
||||||
|
open={openModal}
|
||||||
|
handleOpen={setOpenModal}
|
||||||
|
addPermission={addPermission}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddPermission
|
||||||
@@ -3,31 +3,21 @@ import axios from 'axios'
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
TextField,
|
TextField,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Autocomplete
|
Autocomplete
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
|
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialog } from '../../../../components/modal'
|
||||||
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserResponse,
|
UserResponse,
|
||||||
GroupResponse,
|
GroupResponse,
|
||||||
RegisterPermissionPayload
|
RegisterPermissionPayload
|
||||||
} from '../../utils/types'
|
} from '../../../../utils/types'
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type AddPermissionModalProps = {
|
type AddPermissionModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Typography, Popover } from '@mui/material'
|
||||||
|
import { GroupDetailsResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
|
type DisplayGroupProps = {
|
||||||
|
group: GroupDetailsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePopoverClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = Boolean(anchorEl)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
aria-owns={open ? 'mouse-over-popover' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
onMouseEnter={handlePopoverOpen}
|
||||||
|
onMouseLeave={handlePopoverClose}
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</Typography>
|
||||||
|
<Popover
|
||||||
|
id="mouse-over-popover"
|
||||||
|
sx={{
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
onClose={handlePopoverClose}
|
||||||
|
disableRestoreFocus
|
||||||
|
>
|
||||||
|
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
||||||
|
Group Members
|
||||||
|
</Typography>
|
||||||
|
{group.users.map((user, index) => (
|
||||||
|
<Typography key={index} sx={{ p: 1 }} component="li">
|
||||||
|
{user.username}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DisplayGroup
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { Dispatch, SetStateAction, useState } from 'react'
|
||||||
|
import { IconButton, Tooltip } from '@mui/material'
|
||||||
|
import { FilterList } from '@mui/icons-material'
|
||||||
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
import PermissionFilterModal from './permissionFilterModal'
|
||||||
|
import { PrincipalType } from '../hooks/usePermission'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
applyFilter: (
|
||||||
|
pathFilter: string[],
|
||||||
|
principalFilter: string[],
|
||||||
|
principalTypeFilter: PrincipalType[],
|
||||||
|
settingFilter: string[]
|
||||||
|
) => void
|
||||||
|
resetFilter: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterPermissions = ({
|
||||||
|
open,
|
||||||
|
handleOpen,
|
||||||
|
permissions,
|
||||||
|
applyFilter,
|
||||||
|
resetFilter
|
||||||
|
}: Props) => {
|
||||||
|
const [pathFilter, setPathFilter] = useState<string[]>([])
|
||||||
|
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||||
|
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
||||||
|
PrincipalType[]
|
||||||
|
>([])
|
||||||
|
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
||||||
|
const handleApplyFilter = () => {
|
||||||
|
applyFilter(pathFilter, principalFilter, principalTypeFilter, settingFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetFilter = () => {
|
||||||
|
setPathFilter([])
|
||||||
|
setPrincipalFilter([])
|
||||||
|
setPrincipalFilter([])
|
||||||
|
setSettingFilter([])
|
||||||
|
resetFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Filter Permissions">
|
||||||
|
<IconButton onClick={() => handleOpen(true)}>
|
||||||
|
<FilterList />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<PermissionFilterModal
|
||||||
|
open={open}
|
||||||
|
handleOpen={handleOpen}
|
||||||
|
permissions={permissions}
|
||||||
|
pathFilter={pathFilter}
|
||||||
|
setPathFilter={setPathFilter}
|
||||||
|
principalFilter={principalFilter}
|
||||||
|
setPrincipalFilter={setPrincipalFilter}
|
||||||
|
principalTypeFilter={principalTypeFilter}
|
||||||
|
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
||||||
|
settingFilter={settingFilter}
|
||||||
|
setSettingFilter={setSettingFilter}
|
||||||
|
applyFilter={handleApplyFilter}
|
||||||
|
resetFilter={handleResetFilter}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterPermissions
|
||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
import Autocomplete from '@mui/material/Autocomplete'
|
import Autocomplete from '@mui/material/Autocomplete'
|
||||||
|
|
||||||
import { PermissionResponse } from '../../utils/types'
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
import { PrincipalType } from './permission'
|
import { PrincipalType } from '../hooks/usePermission'
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||||
'& .MuiDialogContent-root': {
|
'& .MuiDialogContent-root': {
|
||||||
@@ -2,9 +2,9 @@ import React from 'react'
|
|||||||
|
|
||||||
import { Typography, DialogContent } from '@mui/material'
|
import { Typography, DialogContent } from '@mui/material'
|
||||||
|
|
||||||
import { BootstrapDialog } from '../../components/modal'
|
import { BootstrapDialog } from '../../../../components/modal'
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
import { PermissionResponse } from '../../utils/types'
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
export interface PermissionResponsePayload {
|
export interface PermissionResponsePayload {
|
||||||
permissionType: string
|
permissionType: string
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
IconButton,
|
||||||
|
Tooltip
|
||||||
|
} from '@mui/material'
|
||||||
|
|
||||||
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
|
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
||||||
|
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
|
import { AppContext } from '../../../../context/appContext'
|
||||||
|
import { displayPrincipal, displayPrincipalType } from '../helper'
|
||||||
|
|
||||||
|
const BootstrapTableCell = styled(TableCell)({
|
||||||
|
textAlign: 'left'
|
||||||
|
})
|
||||||
|
|
||||||
|
export enum PrincipalType {
|
||||||
|
User = 'User',
|
||||||
|
Group = 'Group'
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermissionTableProps = {
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
||||||
|
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionTable = ({
|
||||||
|
permissions,
|
||||||
|
handleUpdatePermissionClick,
|
||||||
|
handleDeletePermissionClick
|
||||||
|
}: PermissionTableProps) => {
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table sx={{ minWidth: 650 }}>
|
||||||
|
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||||
|
<TableRow>
|
||||||
|
<BootstrapTableCell>Path</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||||
|
{appContext.isAdmin && (
|
||||||
|
<BootstrapTableCell>Action</BootstrapTableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{permissions.map((permission) => (
|
||||||
|
<TableRow key={permission.permissionId}>
|
||||||
|
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>
|
||||||
|
{displayPrincipal(permission)}
|
||||||
|
</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>
|
||||||
|
{displayPrincipalType(permission)}
|
||||||
|
</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
||||||
|
{appContext.isAdmin && (
|
||||||
|
<BootstrapTableCell>
|
||||||
|
<Tooltip title="Edit Permission">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => handleUpdatePermissionClick(permission)}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Delete Permission">
|
||||||
|
<IconButton
|
||||||
|
color="error"
|
||||||
|
onClick={() => handleDeletePermissionClick(permission)}
|
||||||
|
>
|
||||||
|
<DeleteForeverIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</BootstrapTableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionTable
|
||||||
@@ -2,26 +2,17 @@ import React, { useState, Dispatch, SetStateAction, useEffect } from 'react'
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
TextField
|
TextField
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
import Autocomplete from '@mui/material/Autocomplete'
|
import Autocomplete from '@mui/material/Autocomplete'
|
||||||
|
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialog } from '../../../../components/modal'
|
||||||
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
|
|
||||||
import { PermissionResponse } from '../../utils/types'
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type UpdatePermissionModalProps = {
|
type UpdatePermissionModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
13
web/src/containers/Settings/internal/helper.tsx
Normal file
13
web/src/containers/Settings/internal/helper.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { PermissionResponse } from '../../../utils/types'
|
||||||
|
import { PrincipalType } from './hooks/usePermission'
|
||||||
|
import DisplayGroup from './components/displayGroup'
|
||||||
|
|
||||||
|
export const displayPrincipal = (permission: PermissionResponse) => {
|
||||||
|
if (permission.user) return permission.user.username
|
||||||
|
if (permission.group) return <DisplayGroup group={permission.group} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayPrincipalType = (permission: PermissionResponse) => {
|
||||||
|
if (permission.user) return PrincipalType.User
|
||||||
|
if (permission.group) return PrincipalType.Group
|
||||||
|
}
|
||||||
109
web/src/containers/Settings/internal/hooks/useAddPermission.tsx
Normal file
109
web/src/containers/Settings/internal/hooks/useAddPermission.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import {
|
||||||
|
PermissionResponse,
|
||||||
|
RegisterPermissionPayload
|
||||||
|
} from '../../../../utils/types'
|
||||||
|
import AddPermission from '../components/addPermission'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import {
|
||||||
|
findExistingPermission,
|
||||||
|
findUpdatingPermission
|
||||||
|
} from '../../../../utils/helper'
|
||||||
|
|
||||||
|
const useAddPermission = () => {
|
||||||
|
const {
|
||||||
|
permissions,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setPermissionResponsePayload,
|
||||||
|
setOpenPermissionResponseModal
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
|
||||||
|
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const addPermission = async (
|
||||||
|
permissionsToAdd: RegisterPermissionPayload[],
|
||||||
|
permissionType: string,
|
||||||
|
principalType: string,
|
||||||
|
principal: string,
|
||||||
|
permissionSetting: string
|
||||||
|
) => {
|
||||||
|
setAddPermissionModalOpen(false)
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const newAddedPermissions: PermissionResponse[] = []
|
||||||
|
const updatedPermissions: PermissionResponse[] = []
|
||||||
|
const errorPaths: string[] = []
|
||||||
|
|
||||||
|
const existingPermissions: PermissionResponse[] = []
|
||||||
|
const updatingPermissions: PermissionResponse[] = []
|
||||||
|
const newPermissions: RegisterPermissionPayload[] = []
|
||||||
|
|
||||||
|
permissionsToAdd.forEach((permission) => {
|
||||||
|
const existingPermission = findExistingPermission(permissions, permission)
|
||||||
|
if (existingPermission) {
|
||||||
|
existingPermissions.push(existingPermission)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatingPermission = findUpdatingPermission(permissions, permission)
|
||||||
|
if (updatingPermission) {
|
||||||
|
updatingPermissions.push(updatingPermission)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPermissions.push(permission)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const permission of newPermissions) {
|
||||||
|
await axios
|
||||||
|
.post('/SASjsApi/permission', permission)
|
||||||
|
.then((res) => {
|
||||||
|
newAddedPermissions.push(res.data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorPaths.push(permission.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const permission of updatingPermissions) {
|
||||||
|
await axios
|
||||||
|
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
|
||||||
|
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
updatedPermissions.push(res.data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorPaths.push(permission.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPermissions()
|
||||||
|
setIsLoading(false)
|
||||||
|
setPermissionResponsePayload({
|
||||||
|
permissionType,
|
||||||
|
principalType,
|
||||||
|
principal,
|
||||||
|
permissionSetting,
|
||||||
|
existingPermissions,
|
||||||
|
updatedPermissions,
|
||||||
|
newAddedPermissions,
|
||||||
|
errorPaths
|
||||||
|
})
|
||||||
|
setOpenPermissionResponseModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddPermissionButton = () => (
|
||||||
|
<AddPermission
|
||||||
|
openModal={addPermissionModalOpen}
|
||||||
|
setOpenModal={setAddPermissionModalOpen}
|
||||||
|
addPermission={addPermission}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { AddPermissionButton, setAddPermissionModalOpen }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAddPermission
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||||
|
import DeleteConfirmationModal from '../../../../components/deleteConfirmationModal'
|
||||||
|
|
||||||
|
const useDeletePermissionModal = () => {
|
||||||
|
const {
|
||||||
|
selectedPermission,
|
||||||
|
setSelectedPermission,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setSnackbarMessage,
|
||||||
|
setSnackbarSeverity,
|
||||||
|
setOpenSnackbar,
|
||||||
|
setModalTitle,
|
||||||
|
setModalPayload,
|
||||||
|
setOpenModal
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const deletePermission = () => {
|
||||||
|
setDeleteConfirmationModalOpen(false)
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||||
|
.then((res: any) => {
|
||||||
|
fetchPermissions()
|
||||||
|
setSnackbarMessage('Permission 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)
|
||||||
|
setSelectedPermission(undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeletePermissionDialog = () => (
|
||||||
|
<DeleteConfirmationModal
|
||||||
|
open={deleteConfirmationModalOpen}
|
||||||
|
setOpen={setDeleteConfirmationModalOpen}
|
||||||
|
message="Are you sure you want to delete this permission?"
|
||||||
|
_delete={deletePermission}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { DeletePermissionDialog, setDeleteConfirmationModalOpen }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDeletePermissionModal
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { PrincipalType } from './usePermission'
|
||||||
|
import FilterPermissions from '../components/filterPermissions'
|
||||||
|
|
||||||
|
const useFilterPermissions = () => {
|
||||||
|
const { permissions, setFilteredPermissions, setFilterApplied } =
|
||||||
|
useContext(PermissionsContext)
|
||||||
|
|
||||||
|
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* first find the permissions w.r.t each filter type
|
||||||
|
* take intersection of resultant arrays
|
||||||
|
*/
|
||||||
|
const applyFilter = (
|
||||||
|
pathFilter: string[],
|
||||||
|
principalFilter: string[],
|
||||||
|
principalTypeFilter: PrincipalType[],
|
||||||
|
settingFilter: string[]
|
||||||
|
) => {
|
||||||
|
setFilterModalOpen(false)
|
||||||
|
|
||||||
|
const uriFilteredPermissions =
|
||||||
|
pathFilter.length > 0
|
||||||
|
? permissions.filter((permission) =>
|
||||||
|
pathFilter.includes(permission.path)
|
||||||
|
)
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
const principalFilteredPermissions =
|
||||||
|
principalFilter.length > 0
|
||||||
|
? permissions.filter((permission) => {
|
||||||
|
if (permission.user) {
|
||||||
|
return principalFilter.includes(permission.user.username)
|
||||||
|
}
|
||||||
|
if (permission.group) {
|
||||||
|
return principalFilter.includes(permission.group.name)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
const principalTypeFilteredPermissions =
|
||||||
|
principalTypeFilter.length > 0
|
||||||
|
? permissions.filter((permission) => {
|
||||||
|
if (permission.user) {
|
||||||
|
return principalTypeFilter.includes(PrincipalType.User)
|
||||||
|
}
|
||||||
|
if (permission.group) {
|
||||||
|
return principalTypeFilter.includes(PrincipalType.Group)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
const settingFilteredPermissions =
|
||||||
|
settingFilter.length > 0
|
||||||
|
? permissions.filter((permission) =>
|
||||||
|
settingFilter.includes(permission.setting)
|
||||||
|
)
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
||||||
|
principalFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
filteredArray = filteredArray.filter((permission) =>
|
||||||
|
principalTypeFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
filteredArray = filteredArray.filter((permission) =>
|
||||||
|
settingFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
setFilteredPermissions(filteredArray)
|
||||||
|
setFilterApplied(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFilter = () => {
|
||||||
|
setFilterModalOpen(false)
|
||||||
|
setFilterApplied(false)
|
||||||
|
setFilteredPermissions([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterPermissionsButton = () => (
|
||||||
|
<FilterPermissions
|
||||||
|
open={filterModalOpen}
|
||||||
|
handleOpen={setFilterModalOpen}
|
||||||
|
permissions={permissions}
|
||||||
|
applyFilter={applyFilter}
|
||||||
|
resetFilter={resetFilter}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { FilterPermissionsButton }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFilterPermissions
|
||||||
71
web/src/containers/Settings/internal/hooks/usePermission.ts
Normal file
71
web/src/containers/Settings/internal/hooks/usePermission.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useContext, useEffect } from 'react'
|
||||||
|
import { AppContext } from '../../../../context/appContext'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
import useAddPermission from './useAddPermission'
|
||||||
|
import useUpdatePermissionModal from './useUpdatePermissionModal'
|
||||||
|
import useDeletePermissionModal from './useDeletePermissionModal'
|
||||||
|
import useFilterPermissions from './useFilterPermissions'
|
||||||
|
|
||||||
|
export enum PrincipalType {
|
||||||
|
User = 'User',
|
||||||
|
Group = 'Group'
|
||||||
|
}
|
||||||
|
|
||||||
|
const usePermission = () => {
|
||||||
|
const { isAdmin } = useContext(AppContext)
|
||||||
|
const {
|
||||||
|
filterApplied,
|
||||||
|
filteredPermissions,
|
||||||
|
isLoading,
|
||||||
|
permissions,
|
||||||
|
Dialog,
|
||||||
|
Snackbar,
|
||||||
|
PermissionResponseDialog,
|
||||||
|
fetchPermissions,
|
||||||
|
setSelectedPermission
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
|
||||||
|
const { AddPermissionButton } = useAddPermission()
|
||||||
|
|
||||||
|
const { UpdatePermissionDialog, setUpdatePermissionModalOpen } =
|
||||||
|
useUpdatePermissionModal()
|
||||||
|
|
||||||
|
const { DeletePermissionDialog, setDeleteConfirmationModalOpen } =
|
||||||
|
useDeletePermissionModal()
|
||||||
|
|
||||||
|
const { FilterPermissionsButton } = useFilterPermissions()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchPermissions) fetchPermissions()
|
||||||
|
}, [fetchPermissions])
|
||||||
|
|
||||||
|
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
||||||
|
setSelectedPermission(permission)
|
||||||
|
setUpdatePermissionModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
||||||
|
setSelectedPermission(permission)
|
||||||
|
setDeleteConfirmationModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filterApplied,
|
||||||
|
filteredPermissions,
|
||||||
|
isAdmin,
|
||||||
|
isLoading,
|
||||||
|
permissions,
|
||||||
|
AddPermissionButton,
|
||||||
|
UpdatePermissionDialog,
|
||||||
|
DeletePermissionDialog,
|
||||||
|
FilterPermissionsButton,
|
||||||
|
handleDeletePermissionClick,
|
||||||
|
handleUpdatePermissionClick,
|
||||||
|
PermissionResponseDialog,
|
||||||
|
Dialog,
|
||||||
|
Snackbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePermission
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import PermissionResponseModal, {
|
||||||
|
PermissionResponsePayload
|
||||||
|
} from '../components/permissionResponseModal'
|
||||||
|
|
||||||
|
const usePermissionResponseModal = () => {
|
||||||
|
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
||||||
|
useState(false)
|
||||||
|
const [permissionResponsePayload, setPermissionResponsePayload] =
|
||||||
|
useState<PermissionResponsePayload>({
|
||||||
|
permissionType: '',
|
||||||
|
principalType: '',
|
||||||
|
principal: '',
|
||||||
|
permissionSetting: '',
|
||||||
|
existingPermissions: [],
|
||||||
|
newAddedPermissions: [],
|
||||||
|
updatedPermissions: [],
|
||||||
|
errorPaths: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const PermissionResponseDialog = () => (
|
||||||
|
<PermissionResponseModal
|
||||||
|
open={openPermissionResponseModal}
|
||||||
|
setOpen={setOpenPermissionResponseModal}
|
||||||
|
payload={permissionResponsePayload}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
PermissionResponseDialog,
|
||||||
|
setOpenPermissionResponseModal,
|
||||||
|
setPermissionResponsePayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePermissionResponseModal
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import UpdatePermissionModal from '../components/updatePermissionModal'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||||
|
|
||||||
|
const useUpdatePermissionModal = () => {
|
||||||
|
const {
|
||||||
|
selectedPermission,
|
||||||
|
setSelectedPermission,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setSnackbarMessage,
|
||||||
|
setSnackbarSeverity,
|
||||||
|
setOpenSnackbar,
|
||||||
|
setModalTitle,
|
||||||
|
setModalPayload,
|
||||||
|
setOpenModal
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const updatePermission = (setting: string) => {
|
||||||
|
setUpdatePermissionModalOpen(false)
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
||||||
|
setting
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
fetchPermissions()
|
||||||
|
setSnackbarMessage('Permission updated!')
|
||||||
|
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)
|
||||||
|
setSelectedPermission(undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdatePermissionDialog = () => (
|
||||||
|
<UpdatePermissionModal
|
||||||
|
open={updatePermissionModalOpen}
|
||||||
|
handleOpen={setUpdatePermissionModalOpen}
|
||||||
|
permission={selectedPermission}
|
||||||
|
updatePermission={updatePermission}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { UpdatePermissionDialog, setUpdatePermissionModalOpen }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUpdatePermissionModal
|
||||||
@@ -1,54 +1,7 @@
|
|||||||
import React, { useState, useEffect, useContext, useCallback } from 'react'
|
import { Box, Paper, Grid, CircularProgress } from '@mui/material'
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Paper,
|
|
||||||
Grid,
|
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
Popover
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
import FilterListIcon from '@mui/icons-material/FilterList'
|
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
|
||||||
|
|
||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
|
import PermissionTable from './internal/components/permissionTable'
|
||||||
import Modal from '../../components/modal'
|
import usePermission from './internal/hooks/usePermission'
|
||||||
import PermissionFilterModal from './permissionFilterModal'
|
|
||||||
import AddPermissionModal from './addPermissionModal'
|
|
||||||
import PermissionResponseModal, {
|
|
||||||
PermissionResponsePayload
|
|
||||||
} from './addPermissionResponseModal'
|
|
||||||
import UpdatePermissionModal from './updatePermissionModal'
|
|
||||||
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
|
||||||
|
|
||||||
import {
|
|
||||||
GroupDetailsResponse,
|
|
||||||
PermissionResponse,
|
|
||||||
RegisterPermissionPayload
|
|
||||||
} from '../../utils/types'
|
|
||||||
import {
|
|
||||||
findExistingPermission,
|
|
||||||
findUpdatingPermission
|
|
||||||
} from '../../utils/helper'
|
|
||||||
|
|
||||||
import { AppContext } from '../../context/appContext'
|
|
||||||
|
|
||||||
const BootstrapTableCell = styled(TableCell)({
|
|
||||||
textAlign: 'left'
|
|
||||||
})
|
|
||||||
|
|
||||||
const BootstrapGridItem = styled(Grid)({
|
const BootstrapGridItem = styled(Grid)({
|
||||||
'&.MuiGrid-item': {
|
'&.MuiGrid-item': {
|
||||||
@@ -56,298 +9,23 @@ const BootstrapGridItem = styled(Grid)({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export enum PrincipalType {
|
|
||||||
User = 'User',
|
|
||||||
Group = 'Group'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Permission = () => {
|
const Permission = () => {
|
||||||
const appContext = useContext(AppContext)
|
const {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
filterApplied,
|
||||||
const [openModal, setOpenModal] = useState(false)
|
filteredPermissions,
|
||||||
const [modalTitle, setModalTitle] = useState('')
|
isAdmin,
|
||||||
const [modalPayload, setModalPayload] = useState('')
|
isLoading,
|
||||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
permissions,
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
AddPermissionButton,
|
||||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
UpdatePermissionDialog,
|
||||||
AlertSeverityType.Success
|
DeletePermissionDialog,
|
||||||
)
|
FilterPermissionsButton,
|
||||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
handleDeletePermissionClick,
|
||||||
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
handleUpdatePermissionClick,
|
||||||
useState(false)
|
PermissionResponseDialog,
|
||||||
const [permissionResponsePayload, setPermissionResponsePayload] =
|
Dialog,
|
||||||
useState<PermissionResponsePayload>({
|
Snackbar
|
||||||
permissionType: '',
|
} = usePermission()
|
||||||
principalType: '',
|
|
||||||
principal: '',
|
|
||||||
permissionSetting: '',
|
|
||||||
existingPermissions: [],
|
|
||||||
newAddedPermissions: [],
|
|
||||||
updatedPermissions: [],
|
|
||||||
errorPaths: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
|
||||||
useState(false)
|
|
||||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
|
||||||
useState(false)
|
|
||||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
|
||||||
useState('')
|
|
||||||
const [selectedPermission, setSelectedPermission] =
|
|
||||||
useState<PermissionResponse>()
|
|
||||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
|
||||||
const [pathFilter, setPathFilter] = useState<string[]>([])
|
|
||||||
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
|
||||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
|
||||||
PrincipalType[]
|
|
||||||
>([])
|
|
||||||
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
|
||||||
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
|
||||||
const [filteredPermissions, setFilteredPermissions] = useState<
|
|
||||||
PermissionResponse[]
|
|
||||||
>([])
|
|
||||||
const [filterApplied, setFilterApplied] = useState(false)
|
|
||||||
|
|
||||||
const fetchPermissions = useCallback(() => {
|
|
||||||
axios
|
|
||||||
.get(`/SASjsApi/permission`)
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.data?.length > 0) {
|
|
||||||
setPermissions(res.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPermissions()
|
|
||||||
}, [fetchPermissions])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* first find the permissions w.r.t each filter type
|
|
||||||
* take intersection of resultant arrays
|
|
||||||
*/
|
|
||||||
const applyFilter = () => {
|
|
||||||
setFilterModalOpen(false)
|
|
||||||
|
|
||||||
const uriFilteredPermissions =
|
|
||||||
pathFilter.length > 0
|
|
||||||
? permissions.filter((permission) =>
|
|
||||||
pathFilter.includes(permission.path)
|
|
||||||
)
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const principalFilteredPermissions =
|
|
||||||
principalFilter.length > 0
|
|
||||||
? permissions.filter((permission) => {
|
|
||||||
if (permission.user) {
|
|
||||||
return principalFilter.includes(permission.user.username)
|
|
||||||
}
|
|
||||||
if (permission.group) {
|
|
||||||
return principalFilter.includes(permission.group.name)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const principalTypeFilteredPermissions =
|
|
||||||
principalTypeFilter.length > 0
|
|
||||||
? permissions.filter((permission) => {
|
|
||||||
if (permission.user) {
|
|
||||||
return principalTypeFilter.includes(PrincipalType.User)
|
|
||||||
}
|
|
||||||
if (permission.group) {
|
|
||||||
return principalTypeFilter.includes(PrincipalType.Group)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const settingFilteredPermissions =
|
|
||||||
settingFilter.length > 0
|
|
||||||
? permissions.filter((permission) =>
|
|
||||||
settingFilter.includes(permission.setting)
|
|
||||||
)
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
|
||||||
principalFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
filteredArray = filteredArray.filter((permission) =>
|
|
||||||
principalTypeFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
filteredArray = filteredArray.filter((permission) =>
|
|
||||||
settingFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
setFilteredPermissions(filteredArray)
|
|
||||||
setFilterApplied(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetFilter = () => {
|
|
||||||
setFilterModalOpen(false)
|
|
||||||
setPathFilter([])
|
|
||||||
setPrincipalFilter([])
|
|
||||||
setSettingFilter([])
|
|
||||||
setFilteredPermissions([])
|
|
||||||
setFilterApplied(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPermission = async (
|
|
||||||
permissionsToAdd: RegisterPermissionPayload[],
|
|
||||||
permissionType: string,
|
|
||||||
principalType: string,
|
|
||||||
principal: string,
|
|
||||||
permissionSetting: string
|
|
||||||
) => {
|
|
||||||
setAddPermissionModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const newAddedPermissions: PermissionResponse[] = []
|
|
||||||
const updatedPermissions: PermissionResponse[] = []
|
|
||||||
const errorPaths: string[] = []
|
|
||||||
|
|
||||||
const existingPermissions: PermissionResponse[] = []
|
|
||||||
const updatingPermissions: PermissionResponse[] = []
|
|
||||||
const newPermissions: RegisterPermissionPayload[] = []
|
|
||||||
|
|
||||||
permissionsToAdd.forEach((permission) => {
|
|
||||||
const existingPermission = findExistingPermission(permissions, permission)
|
|
||||||
if (existingPermission) {
|
|
||||||
existingPermissions.push(existingPermission)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatingPermission = findUpdatingPermission(permissions, permission)
|
|
||||||
if (updatingPermission) {
|
|
||||||
updatingPermissions.push(updatingPermission)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newPermissions.push(permission)
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const permission of newPermissions) {
|
|
||||||
await axios
|
|
||||||
.post('/SASjsApi/permission', permission)
|
|
||||||
.then((res) => {
|
|
||||||
newAddedPermissions.push(res.data)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
errorPaths.push(permission.path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const permission of updatingPermissions) {
|
|
||||||
await axios
|
|
||||||
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
|
|
||||||
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
updatedPermissions.push(res.data)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
errorPaths.push(permission.path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchPermissions()
|
|
||||||
setIsLoading(false)
|
|
||||||
setPermissionResponsePayload({
|
|
||||||
permissionType,
|
|
||||||
principalType,
|
|
||||||
principal,
|
|
||||||
permissionSetting,
|
|
||||||
existingPermissions,
|
|
||||||
updatedPermissions,
|
|
||||||
newAddedPermissions,
|
|
||||||
errorPaths
|
|
||||||
})
|
|
||||||
setOpenPermissionResponseModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
|
||||||
setSelectedPermission(permission)
|
|
||||||
setUpdatePermissionModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePermission = (setting: string) => {
|
|
||||||
setUpdatePermissionModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
|
||||||
setting
|
|
||||||
})
|
|
||||||
.then((res: any) => {
|
|
||||||
fetchPermissions()
|
|
||||||
setSnackbarMessage('Permission updated!')
|
|
||||||
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)
|
|
||||||
setSelectedPermission(undefined)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
|
||||||
setSelectedPermission(permission)
|
|
||||||
setDeleteConfirmationModalOpen(true)
|
|
||||||
setDeleteConfirmationModalMessage(
|
|
||||||
'Are you sure you want to delete this permission?'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletePermission = () => {
|
|
||||||
setDeleteConfirmationModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
|
||||||
.then((res: any) => {
|
|
||||||
fetchPermissions()
|
|
||||||
setSnackbarMessage('Permission 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)
|
|
||||||
setSelectedPermission(undefined)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
@@ -358,22 +36,8 @@ const Permission = () => {
|
|||||||
<Grid container direction="column" spacing={1}>
|
<Grid container direction="column" spacing={1}>
|
||||||
<BootstrapGridItem item xs={12}>
|
<BootstrapGridItem item xs={12}>
|
||||||
<Paper elevation={3} sx={{ display: 'flex' }}>
|
<Paper elevation={3} sx={{ display: 'flex' }}>
|
||||||
<Tooltip title="Filter Permissions">
|
<FilterPermissionsButton />
|
||||||
<IconButton onClick={() => setFilterModalOpen(true)}>
|
{isAdmin && <AddPermissionButton />}
|
||||||
<FilterListIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<Tooltip
|
|
||||||
sx={{ marginLeft: 'auto' }}
|
|
||||||
title="Add Permission"
|
|
||||||
placement="bottom-end"
|
|
||||||
>
|
|
||||||
<IconButton onClick={() => setAddPermissionModalOpen(true)}>
|
|
||||||
<AddIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</BootstrapGridItem>
|
</BootstrapGridItem>
|
||||||
<BootstrapGridItem item xs={12}>
|
<BootstrapGridItem item xs={12}>
|
||||||
@@ -384,192 +48,13 @@ const Permission = () => {
|
|||||||
/>
|
/>
|
||||||
</BootstrapGridItem>
|
</BootstrapGridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
<BootstrapSnackbar
|
<PermissionResponseDialog />
|
||||||
open={openSnackbar}
|
<UpdatePermissionDialog />
|
||||||
setOpen={setOpenSnackbar}
|
<DeletePermissionDialog />
|
||||||
message={snackbarMessage}
|
<Dialog />
|
||||||
severity={snackbarSeverity}
|
<Snackbar />
|
||||||
/>
|
|
||||||
<Modal
|
|
||||||
open={openModal}
|
|
||||||
setOpen={setOpenModal}
|
|
||||||
title={modalTitle}
|
|
||||||
payload={modalPayload}
|
|
||||||
/>
|
|
||||||
<PermissionFilterModal
|
|
||||||
open={filterModalOpen}
|
|
||||||
handleOpen={setFilterModalOpen}
|
|
||||||
permissions={permissions}
|
|
||||||
pathFilter={pathFilter}
|
|
||||||
setPathFilter={setPathFilter}
|
|
||||||
principalFilter={principalFilter}
|
|
||||||
setPrincipalFilter={setPrincipalFilter}
|
|
||||||
principalTypeFilter={principalTypeFilter}
|
|
||||||
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
|
||||||
settingFilter={settingFilter}
|
|
||||||
setSettingFilter={setSettingFilter}
|
|
||||||
applyFilter={applyFilter}
|
|
||||||
resetFilter={resetFilter}
|
|
||||||
/>
|
|
||||||
<AddPermissionModal
|
|
||||||
open={addPermissionModalOpen}
|
|
||||||
handleOpen={setAddPermissionModalOpen}
|
|
||||||
addPermission={addPermission}
|
|
||||||
/>
|
|
||||||
<PermissionResponseModal
|
|
||||||
open={openPermissionResponseModal}
|
|
||||||
setOpen={setOpenPermissionResponseModal}
|
|
||||||
payload={permissionResponsePayload}
|
|
||||||
/>
|
|
||||||
<UpdatePermissionModal
|
|
||||||
open={updatePermissionModalOpen}
|
|
||||||
handleOpen={setUpdatePermissionModalOpen}
|
|
||||||
permission={selectedPermission}
|
|
||||||
updatePermission={updatePermission}
|
|
||||||
/>
|
|
||||||
<DeleteConfirmationModal
|
|
||||||
open={deleteConfirmationModalOpen}
|
|
||||||
setOpen={setDeleteConfirmationModalOpen}
|
|
||||||
message={deleteConfirmationModalMessage}
|
|
||||||
_delete={deletePermission}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Permission
|
export default Permission
|
||||||
|
|
||||||
type PermissionTableProps = {
|
|
||||||
permissions: PermissionResponse[]
|
|
||||||
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
|
||||||
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PermissionTable = ({
|
|
||||||
permissions,
|
|
||||||
handleUpdatePermissionClick,
|
|
||||||
handleDeletePermissionClick
|
|
||||||
}: PermissionTableProps) => {
|
|
||||||
const appContext = useContext(AppContext)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table sx={{ minWidth: 650 }}>
|
|
||||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
|
||||||
<TableRow>
|
|
||||||
<BootstrapTableCell>Path</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{permissions.map((permission) => (
|
|
||||||
<TableRow key={permission.permissionId}>
|
|
||||||
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>
|
|
||||||
{displayPrincipal(permission)}
|
|
||||||
</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>
|
|
||||||
{displayPrincipalType(permission)}
|
|
||||||
</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<BootstrapTableCell>
|
|
||||||
<Tooltip title="Edit Permission">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => handleUpdatePermissionClick(permission)}
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Delete Permission">
|
|
||||||
<IconButton
|
|
||||||
color="error"
|
|
||||||
onClick={() => handleDeletePermissionClick(permission)}
|
|
||||||
>
|
|
||||||
<DeleteForeverIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</BootstrapTableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayPrincipal = (permission: PermissionResponse) => {
|
|
||||||
if (permission.user) return permission.user.username
|
|
||||||
if (permission.group) return <DisplayGroup group={permission.group} />
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplayGroupProps = {
|
|
||||||
group: GroupDetailsResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePopoverClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = Boolean(anchorEl)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Typography
|
|
||||||
aria-owns={open ? 'mouse-over-popover' : undefined}
|
|
||||||
aria-haspopup="true"
|
|
||||||
onMouseEnter={handlePopoverOpen}
|
|
||||||
onMouseLeave={handlePopoverClose}
|
|
||||||
>
|
|
||||||
{group.name}
|
|
||||||
</Typography>
|
|
||||||
<Popover
|
|
||||||
id="mouse-over-popover"
|
|
||||||
sx={{
|
|
||||||
pointerEvents: 'none'
|
|
||||||
}}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
onClose={handlePopoverClose}
|
|
||||||
disableRestoreFocus
|
|
||||||
>
|
|
||||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
|
||||||
Group Members
|
|
||||||
</Typography>
|
|
||||||
{group.users.map((user, index) => (
|
|
||||||
<Typography key={index} sx={{ p: 1 }} component="li">
|
|
||||||
{user.username}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayPrincipalType = (permission: PermissionResponse) => {
|
|
||||||
if (permission.user) return PrincipalType.User
|
|
||||||
if (permission.group) return PrincipalType.Group
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,54 +1,26 @@
|
|||||||
import React, {
|
import React, { Dispatch, SetStateAction } from 'react'
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useContext
|
|
||||||
} from 'react'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Backdrop,
|
Backdrop,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
Tab,
|
Tab,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
import {
|
import Editor, { MonacoDiffEditor } from 'react-monaco-editor'
|
||||||
RocketLaunch,
|
|
||||||
MoreVert,
|
|
||||||
Save,
|
|
||||||
SaveAs,
|
|
||||||
Difference,
|
|
||||||
Edit
|
|
||||||
} from '@mui/icons-material'
|
|
||||||
import Editor, {
|
|
||||||
MonacoDiffEditor,
|
|
||||||
DiffEditorDidMount,
|
|
||||||
EditorDidMount,
|
|
||||||
monaco
|
|
||||||
} from 'react-monaco-editor'
|
|
||||||
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||||
|
|
||||||
import { AppContext, RunTimeType } from '../../context/appContext'
|
|
||||||
|
|
||||||
import FilePathInputModal from '../../components/filePathInputModal'
|
import FilePathInputModal from '../../components/filePathInputModal'
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
import FileMenu from './internal/components/fileMenu'
|
||||||
import Modal from '../../components/modal'
|
import RunMenu from './internal/components/runMenu'
|
||||||
|
|
||||||
import { usePrompt, useStateWithCallback } from '../../utils/hooks'
|
import { usePrompt } from '../../utils/hooks'
|
||||||
|
import { getLanguageFromExtension } from './internal/helper'
|
||||||
|
import useEditor from './internal/hooks/useEditor'
|
||||||
|
|
||||||
const StyledTabPanel = styled(TabPanel)(() => ({
|
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||||
padding: '10px'
|
padding: '10px'
|
||||||
@@ -69,241 +41,77 @@ type SASjsEditorProps = {
|
|||||||
setTab: Dispatch<SetStateAction<string>>
|
setTab: Dispatch<SetStateAction<string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = window.location.origin
|
|
||||||
|
|
||||||
const SASjsEditor = ({
|
const SASjsEditor = ({
|
||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
setSelectedFilePath,
|
setSelectedFilePath,
|
||||||
tab,
|
tab,
|
||||||
setTab
|
setTab
|
||||||
}: SASjsEditorProps) => {
|
}: SASjsEditorProps) => {
|
||||||
const appContext = useContext(AppContext)
|
const {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
ctrlPressed,
|
||||||
const [openModal, setOpenModal] = useState(false)
|
fileContent,
|
||||||
const [modalTitle, setModalTitle] = useState('')
|
isLoading,
|
||||||
const [modalPayload, setModalPayload] = useState('')
|
log,
|
||||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
openFilePathInputModal,
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
prevFileContent,
|
||||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
runTimes,
|
||||||
AlertSeverityType.Success
|
selectedFileExtension,
|
||||||
)
|
selectedRunTime,
|
||||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
showDiff,
|
||||||
const [fileContent, setFileContent] = useState('')
|
webout,
|
||||||
const [log, setLog] = useState('')
|
Dialog,
|
||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
handleChangeRunTime,
|
||||||
const [webout, setWebout] = useState('')
|
handleDiffEditorDidMount,
|
||||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
handleEditorDidMount,
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
handleFilePathInput,
|
||||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
handleKeyDown,
|
||||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
handleKeyUp,
|
||||||
const [showDiff, setShowDiff] = useState(false)
|
handleRunBtnClick,
|
||||||
|
handleTabChange,
|
||||||
const editorRef = useRef(null as any)
|
saveFile,
|
||||||
|
setShowDiff,
|
||||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
setOpenFilePathInputModal,
|
||||||
editorRef.current = editor
|
setFileContent,
|
||||||
editor.focus()
|
Snackbar
|
||||||
editor.addAction({
|
} = useEditor({ selectedFilePath, setSelectedFilePath, setTab })
|
||||||
// An unique identifier of the contributed action.
|
|
||||||
id: 'show-difference',
|
|
||||||
|
|
||||||
// A label of the action that will be presented to the user.
|
|
||||||
label: 'Show Differences',
|
|
||||||
|
|
||||||
// An optional array of keybindings for the action.
|
|
||||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
|
||||||
|
|
||||||
contextMenuGroupId: 'navigation',
|
|
||||||
|
|
||||||
contextMenuOrder: 1,
|
|
||||||
|
|
||||||
// Method that will be executed when the action is triggered.
|
|
||||||
// @param editor The editor instance is passed in as a convenience
|
|
||||||
run: function (ed) {
|
|
||||||
setShowDiff(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
|
||||||
diffEditor.focus()
|
|
||||||
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
|
|
||||||
setShowDiff(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
usePrompt(
|
usePrompt(
|
||||||
'Changes you made may not be saved.',
|
'Changes you made may not be saved.',
|
||||||
prevFileContent !== fileContent && !!selectedFilePath
|
prevFileContent !== fileContent && !!selectedFilePath
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const fileMenu = (
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
<FileMenu
|
||||||
}, [appContext.runTimes])
|
showDiff={showDiff}
|
||||||
|
setShowDiff={setShowDiff}
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
|
currentFileContent={fileContent}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||||
|
saveFile={saveFile}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const monacoEditor = showDiff ? (
|
||||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
<MonacoDiffEditor
|
||||||
}, [runTimes])
|
height="98%"
|
||||||
|
language={getLanguageFromExtension(selectedFileExtension)}
|
||||||
useEffect(() => {
|
original={prevFileContent}
|
||||||
if (selectedFilePath) {
|
value={fileContent}
|
||||||
setIsLoading(true)
|
editorDidMount={handleDiffEditorDidMount}
|
||||||
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
|
options={{ readOnly: ctrlPressed }}
|
||||||
axios
|
onChange={(val) => setFileContent(val)}
|
||||||
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
|
/>
|
||||||
.then((res: any) => {
|
) : (
|
||||||
setPrevFileContent(res.data)
|
<Editor
|
||||||
setFileContent(res.data)
|
height="98%"
|
||||||
})
|
language={getLanguageFromExtension(selectedFileExtension)}
|
||||||
.catch((err) => {
|
value={fileContent}
|
||||||
setModalTitle('Abort')
|
editorDidMount={handleEditorDidMount}
|
||||||
setModalPayload(
|
options={{ readOnly: ctrlPressed }}
|
||||||
typeof err.response.data === 'object'
|
onChange={(val) => setFileContent(val)}
|
||||||
? JSON.stringify(err.response.data)
|
/>
|
||||||
: err.response.data
|
)
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
.finally(() => setIsLoading(false))
|
|
||||||
} else {
|
|
||||||
const content = localStorage.getItem('fileContent') ?? ''
|
|
||||||
setFileContent(content)
|
|
||||||
}
|
|
||||||
setLog('')
|
|
||||||
setWebout('')
|
|
||||||
setTab('code')
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedFilePath])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fileContent.length && !selectedFilePath) {
|
|
||||||
localStorage.setItem('fileContent', fileContent)
|
|
||||||
}
|
|
||||||
}, [fileContent, selectedFilePath])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (runTimes.includes(selectedFileExtension))
|
|
||||||
setSelectedRunTime(selectedFileExtension)
|
|
||||||
}, [selectedFileExtension, runTimes])
|
|
||||||
|
|
||||||
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('log')
|
|
||||||
|
|
||||||
// Scroll to bottom of log
|
|
||||||
const logElement = document.getElementById('log')
|
|
||||||
if (logElement) logElement.scrollTop = logElement.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)
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && fileContent === prevFileContent) {
|
|
||||||
// when fileContent and prevFileContent is same,
|
|
||||||
// callback function in setPrevFileContent method is not called
|
|
||||||
// because behind the scene useEffect hook is being used
|
|
||||||
// for calling callback function, and it's only fired when the
|
|
||||||
// new value is not equal to old value.
|
|
||||||
// So, we'll have to explicitly update the selected file path
|
|
||||||
|
|
||||||
setSelectedFilePath(filePath, true)
|
|
||||||
} else {
|
|
||||||
setPrevFileContent(fileContent, () => {
|
|
||||||
if (filePath) {
|
|
||||||
setSelectedFilePath(filePath, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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 (
|
return (
|
||||||
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||||
@@ -316,15 +124,7 @@ const SASjsEditor = ({
|
|||||||
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
|
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
|
||||||
<Box sx={{ marginTop: '10px' }}>
|
<Box sx={{ marginTop: '10px' }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<FileMenu
|
{fileMenu}
|
||||||
showDiff={showDiff}
|
|
||||||
setShowDiff={setShowDiff}
|
|
||||||
prevFileContent={prevFileContent}
|
|
||||||
currentFileContent={fileContent}
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
|
||||||
saveFile={saveFile}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
@@ -336,26 +136,7 @@ const SASjsEditor = ({
|
|||||||
}}
|
}}
|
||||||
elevation={3}
|
elevation={3}
|
||||||
>
|
>
|
||||||
{showDiff ? (
|
{monacoEditor}
|
||||||
<MonacoDiffEditor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
original={prevFileContent}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleDiffEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Editor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
@@ -392,15 +173,7 @@ const SASjsEditor = ({
|
|||||||
handleChangeRunTime={handleChangeRunTime}
|
handleChangeRunTime={handleChangeRunTime}
|
||||||
handleRunBtnClick={handleRunBtnClick}
|
handleRunBtnClick={handleRunBtnClick}
|
||||||
/>
|
/>
|
||||||
<FileMenu
|
{fileMenu}
|
||||||
showDiff={showDiff}
|
|
||||||
setShowDiff={setShowDiff}
|
|
||||||
prevFileContent={prevFileContent}
|
|
||||||
currentFileContent={fileContent}
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
|
||||||
saveFile={saveFile}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Paper
|
<Paper
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
@@ -413,26 +186,7 @@ const SASjsEditor = ({
|
|||||||
}}
|
}}
|
||||||
elevation={3}
|
elevation={3}
|
||||||
>
|
>
|
||||||
{showDiff ? (
|
{monacoEditor}
|
||||||
<MonacoDiffEditor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
original={prevFileContent}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleDiffEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Editor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -462,18 +216,8 @@ const SASjsEditor = ({
|
|||||||
</StyledTabPanel>
|
</StyledTabPanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
)}
|
)}
|
||||||
<Modal
|
<Dialog />
|
||||||
open={openModal}
|
<Snackbar />
|
||||||
setOpen={setOpenModal}
|
|
||||||
title={modalTitle}
|
|
||||||
payload={modalPayload}
|
|
||||||
/>
|
|
||||||
<BootstrapSnackbar
|
|
||||||
open={openSnackbar}
|
|
||||||
setOpen={setOpenSnackbar}
|
|
||||||
message={snackbarMessage}
|
|
||||||
severity={snackbarSeverity}
|
|
||||||
/>
|
|
||||||
<FilePathInputModal
|
<FilePathInputModal
|
||||||
open={openFilePathInputModal}
|
open={openFilePathInputModal}
|
||||||
setOpen={setOpenFilePathInputModal}
|
setOpen={setOpenFilePathInputModal}
|
||||||
@@ -484,203 +228,3 @@ const SASjsEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default SASjsEditor
|
export default SASjsEditor
|
||||||
|
|
||||||
type RunMenuProps = {
|
|
||||||
selectedFilePath: string
|
|
||||||
fileContent: string
|
|
||||||
prevFileContent: string
|
|
||||||
selectedRunTime: string
|
|
||||||
runTimes: string[]
|
|
||||||
handleChangeRunTime: (event: SelectChangeEvent) => void
|
|
||||||
handleRunBtnClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const RunMenu = ({
|
|
||||||
selectedFilePath,
|
|
||||||
fileContent,
|
|
||||||
prevFileContent,
|
|
||||||
selectedRunTime,
|
|
||||||
runTimes,
|
|
||||||
handleChangeRunTime,
|
|
||||||
handleRunBtnClick
|
|
||||||
}: RunMenuProps) => {
|
|
||||||
const launchProgram = () => {
|
|
||||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip title="CTRL+ENTER will also run code">
|
|
||||||
<Button
|
|
||||||
onClick={handleRunBtnClick}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '5px 5px',
|
|
||||||
minWidth: 'unset'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
draggable="false"
|
|
||||||
style={{ width: '25px' }}
|
|
||||||
src="/running-sas.png"
|
|
||||||
></img>
|
|
||||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
{selectedFilePath ? (
|
|
||||||
<Box sx={{ marginLeft: '10px' }}>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
fileContent !== prevFileContent
|
|
||||||
? 'Save file before launching program'
|
|
||||||
: 'Launch program in new window'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<IconButton
|
|
||||||
disabled={fileContent !== prevFileContent}
|
|
||||||
onClick={launchProgram}
|
|
||||||
>
|
|
||||||
<RocketLaunch />
|
|
||||||
</IconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
|
||||||
<FormControl variant="standard">
|
|
||||||
<Select
|
|
||||||
labelId="run-time-select-label"
|
|
||||||
id="run-time-select"
|
|
||||||
value={selectedRunTime}
|
|
||||||
onChange={handleChangeRunTime}
|
|
||||||
>
|
|
||||||
{runTimes.map((runTime) => (
|
|
||||||
<MenuItem key={runTime} value={runTime}>
|
|
||||||
{runTime}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileMenuProps = {
|
|
||||||
showDiff: boolean
|
|
||||||
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
prevFileContent: string
|
|
||||||
currentFileContent: string
|
|
||||||
selectedFilePath: string
|
|
||||||
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
saveFile: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileMenu = ({
|
|
||||||
showDiff,
|
|
||||||
setShowDiff,
|
|
||||||
prevFileContent,
|
|
||||||
currentFileContent,
|
|
||||||
selectedFilePath,
|
|
||||||
setOpenFilePathInputModal,
|
|
||||||
saveFile
|
|
||||||
}: FileMenuProps) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<
|
|
||||||
(EventTarget & HTMLButtonElement) | null
|
|
||||||
>(null)
|
|
||||||
|
|
||||||
const handleMenu = (
|
|
||||||
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
if (event) setAnchorEl(event.currentTarget)
|
|
||||||
else setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiffBtnClick = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
setShowDiff(!showDiff)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveAsBtnClick = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
setOpenFilePathInputModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveBtnClick = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
saveFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip title="Save File Menu">
|
|
||||||
<IconButton onClick={handleMenu}>
|
|
||||||
<MoreVert />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Menu
|
|
||||||
id="save-file-menu"
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
open={!!anchorEl}
|
|
||||||
onClose={() => handleMenu()}
|
|
||||||
>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
onClick={handleDiffBtnClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={showDiff ? <Edit /> : <Difference />}
|
|
||||||
>
|
|
||||||
{showDiff ? 'Edit' : 'Diff'}
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveBtnClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<Save />}
|
|
||||||
disabled={
|
|
||||||
!selectedFilePath || prevFileContent === currentFileContent
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveAsBtnClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<SaveAs />}
|
|
||||||
>
|
|
||||||
Save As
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLanguage = (extension: string) => {
|
|
||||||
if (extension === 'js') return 'javascript'
|
|
||||||
|
|
||||||
if (extension === 'ts') return 'typescript'
|
|
||||||
|
|
||||||
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
|
||||||
|
|
||||||
return extension
|
|
||||||
}
|
|
||||||
|
|||||||
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { Button, IconButton, Menu, MenuItem, Tooltip } from '@mui/material'
|
||||||
|
|
||||||
|
import { MoreVert, Save, SaveAs, Difference, Edit } from '@mui/icons-material'
|
||||||
|
|
||||||
|
type FileMenuProps = {
|
||||||
|
showDiff: boolean
|
||||||
|
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
prevFileContent: string
|
||||||
|
currentFileContent: string
|
||||||
|
selectedFilePath: string
|
||||||
|
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
saveFile: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileMenu = ({
|
||||||
|
showDiff,
|
||||||
|
setShowDiff,
|
||||||
|
prevFileContent,
|
||||||
|
currentFileContent,
|
||||||
|
selectedFilePath,
|
||||||
|
setOpenFilePathInputModal,
|
||||||
|
saveFile
|
||||||
|
}: FileMenuProps) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
|
(EventTarget & HTMLButtonElement) | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const handleMenu = (
|
||||||
|
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
if (event) setAnchorEl(event.currentTarget)
|
||||||
|
else setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiffBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setShowDiff(!showDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveAsBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setOpenFilePathInputModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
saveFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Save File Menu">
|
||||||
|
<IconButton onClick={handleMenu}>
|
||||||
|
<MoreVert />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
id="save-file-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
open={!!anchorEl}
|
||||||
|
onClose={() => handleMenu()}
|
||||||
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleDiffBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={showDiff ? <Edit /> : <Difference />}
|
||||||
|
>
|
||||||
|
{showDiff ? 'Edit' : 'Diff'}
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<Save />}
|
||||||
|
disabled={
|
||||||
|
!selectedFilePath || prevFileContent === currentFileContent
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveAsBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SaveAs />}
|
||||||
|
>
|
||||||
|
Save As
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileMenu
|
||||||
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Tooltip
|
||||||
|
} from '@mui/material'
|
||||||
|
|
||||||
|
import { RocketLaunch } from '@mui/icons-material'
|
||||||
|
|
||||||
|
type RunMenuProps = {
|
||||||
|
selectedFilePath: string
|
||||||
|
fileContent: string
|
||||||
|
prevFileContent: string
|
||||||
|
selectedRunTime: string
|
||||||
|
runTimes: string[]
|
||||||
|
handleChangeRunTime: (event: SelectChangeEvent) => void
|
||||||
|
handleRunBtnClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunMenu = ({
|
||||||
|
selectedFilePath,
|
||||||
|
fileContent,
|
||||||
|
prevFileContent,
|
||||||
|
selectedRunTime,
|
||||||
|
runTimes,
|
||||||
|
handleChangeRunTime,
|
||||||
|
handleRunBtnClick
|
||||||
|
}: RunMenuProps) => {
|
||||||
|
const launchProgram = () => {
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="CTRL+ENTER will also run code">
|
||||||
|
<Button
|
||||||
|
onClick={handleRunBtnClick}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '5px 5px',
|
||||||
|
minWidth: 'unset'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
draggable="false"
|
||||||
|
style={{ width: '25px' }}
|
||||||
|
src="/running-sas.png"
|
||||||
|
></img>
|
||||||
|
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{selectedFilePath ? (
|
||||||
|
<Box sx={{ marginLeft: '10px' }}>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
fileContent !== prevFileContent
|
||||||
|
? 'Save file before launching program'
|
||||||
|
: 'Launch program in new window'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={fileContent !== prevFileContent}
|
||||||
|
onClick={launchProgram}
|
||||||
|
>
|
||||||
|
<RocketLaunch />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
||||||
|
<FormControl variant="standard">
|
||||||
|
<Select
|
||||||
|
labelId="run-time-select-label"
|
||||||
|
id="run-time-select"
|
||||||
|
value={selectedRunTime}
|
||||||
|
onChange={handleChangeRunTime}
|
||||||
|
>
|
||||||
|
{runTimes.map((runTime) => (
|
||||||
|
<MenuItem key={runTime} value={runTime}>
|
||||||
|
{runTime}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RunMenu
|
||||||
14
web/src/containers/Studio/internal/helper.ts
Normal file
14
web/src/containers/Studio/internal/helper.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const getLanguageFromExtension = (extension: string) => {
|
||||||
|
if (extension === 'js') return 'javascript'
|
||||||
|
|
||||||
|
if (extension === 'ts') return 'typescript'
|
||||||
|
|
||||||
|
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
||||||
|
|
||||||
|
return extension
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSelection = (editor: any) => {
|
||||||
|
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||||
|
return selection ?? ''
|
||||||
|
}
|
||||||
299
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
299
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
|
import { DiffEditorDidMount, EditorDidMount, monaco } from 'react-monaco-editor'
|
||||||
|
import { SelectChangeEvent } from '@mui/material'
|
||||||
|
import { getSelection } from '../helper'
|
||||||
|
import { AppContext, RunTimeType } from '../../../../context/appContext'
|
||||||
|
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||||
|
import {
|
||||||
|
useModal,
|
||||||
|
useSnackbar,
|
||||||
|
useStateWithCallback
|
||||||
|
} from '../../../../utils/hooks'
|
||||||
|
|
||||||
|
const SASJS_LOGS_SEPARATOR =
|
||||||
|
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
|
type UseEditorParams = {
|
||||||
|
selectedFilePath: string
|
||||||
|
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||||
|
setTab: Dispatch<SetStateAction<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const useEditor = ({
|
||||||
|
selectedFilePath,
|
||||||
|
setSelectedFilePath,
|
||||||
|
setTab
|
||||||
|
}: UseEditorParams) => {
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
|
||||||
|
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||||
|
useSnackbar()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||||
|
const [fileContent, setFileContent] = useState('')
|
||||||
|
const [log, setLog] = useState('')
|
||||||
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
|
const [webout, setWebout] = useState('')
|
||||||
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
|
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 handleEditorDidMount: EditorDidMount = (editor) => {
|
||||||
|
editorRef.current = editor
|
||||||
|
editor.focus()
|
||||||
|
editor.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'show-difference',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Show Differences',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
||||||
|
|
||||||
|
contextMenuGroupId: 'navigation',
|
||||||
|
|
||||||
|
contextMenuOrder: 1,
|
||||||
|
|
||||||
|
// Method that will be executed when the action is triggered.
|
||||||
|
// @param editor The editor instance is passed in as a convenience
|
||||||
|
run: function (ed) {
|
||||||
|
setShowDiff(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
||||||
|
diffEditor.focus()
|
||||||
|
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
|
||||||
|
setShowDiff(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFile = useCallback(
|
||||||
|
(filePath?: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob)
|
||||||
|
formData.append('filePath', filePath ?? selectedFilePath)
|
||||||
|
|
||||||
|
const axiosPromise = filePath
|
||||||
|
? axios.post('/SASjsApi/drive/file', formData)
|
||||||
|
: axios.patch('/SASjsApi/drive/file', formData)
|
||||||
|
|
||||||
|
axiosPromise
|
||||||
|
.then(() => {
|
||||||
|
if (filePath && fileContent === prevFileContent) {
|
||||||
|
// when fileContent and prevFileContent is same,
|
||||||
|
// callback function in setPrevFileContent method is not called
|
||||||
|
// because behind the scene useEffect hook is being used
|
||||||
|
// for calling callback function, and it's only fired when the
|
||||||
|
// new value is not equal to old value.
|
||||||
|
// So, we'll have to explicitly update the selected file path
|
||||||
|
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
} else {
|
||||||
|
setPrevFileContent(fileContent, () => {
|
||||||
|
if (filePath) {
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[fileContent, prevFileContent, selectedFilePath]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTabChange = (_e: any, newValue: string) => {
|
||||||
|
setTab(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRunBtnClick = () =>
|
||||||
|
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||||
|
|
||||||
|
const runCode = (code: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||||
|
.then((res: any) => {
|
||||||
|
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||||
|
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||||
|
setTab('log')
|
||||||
|
|
||||||
|
// Scroll to bottom of log
|
||||||
|
const logElement = document.getElementById('log')
|
||||||
|
if (logElement) logElement.scrollTop = logElement.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(editorRef.current as any) || 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorRef.current.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'save-file',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Save',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||||
|
|
||||||
|
// Method that will be executed when the action is triggered.
|
||||||
|
// @param editor The editor instance is passed in as a convenience
|
||||||
|
run: () => {
|
||||||
|
if (!selectedFilePath) return setOpenFilePathInputModal(true)
|
||||||
|
if (prevFileContent !== fileContent) return saveFile()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
|
||||||
|
|
||||||
|
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))
|
||||||
|
} else {
|
||||||
|
const content = localStorage.getItem('fileContent') ?? ''
|
||||||
|
setFileContent(content)
|
||||||
|
}
|
||||||
|
setLog('')
|
||||||
|
setWebout('')
|
||||||
|
setTab('code')
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedFilePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileContent.length && !selectedFilePath) {
|
||||||
|
localStorage.setItem('fileContent', fileContent)
|
||||||
|
}
|
||||||
|
}, [fileContent, selectedFilePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runTimes.includes(selectedFileExtension))
|
||||||
|
setSelectedRunTime(selectedFileExtension)
|
||||||
|
}, [selectedFileExtension, runTimes])
|
||||||
|
|
||||||
|
return {
|
||||||
|
ctrlPressed,
|
||||||
|
fileContent,
|
||||||
|
isLoading,
|
||||||
|
log,
|
||||||
|
openFilePathInputModal,
|
||||||
|
prevFileContent,
|
||||||
|
runTimes,
|
||||||
|
selectedFileExtension,
|
||||||
|
selectedRunTime,
|
||||||
|
showDiff,
|
||||||
|
webout,
|
||||||
|
Dialog,
|
||||||
|
handleChangeRunTime,
|
||||||
|
handleDiffEditorDidMount,
|
||||||
|
handleEditorDidMount,
|
||||||
|
handleFilePathInput,
|
||||||
|
handleKeyDown,
|
||||||
|
handleKeyUp,
|
||||||
|
handleRunBtnClick,
|
||||||
|
handleTabChange,
|
||||||
|
saveFile,
|
||||||
|
setShowDiff,
|
||||||
|
setOpenFilePathInputModal,
|
||||||
|
setFileContent,
|
||||||
|
Snackbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useEditor
|
||||||
@@ -16,7 +16,9 @@ export enum ModeType {
|
|||||||
|
|
||||||
export enum RunTimeType {
|
export enum RunTimeType {
|
||||||
SAS = 'sas',
|
SAS = 'sas',
|
||||||
JS = 'js'
|
JS = 'js',
|
||||||
|
PY = 'py',
|
||||||
|
R = 'r'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppContextProps {
|
interface AppContextProps {
|
||||||
|
|||||||
120
web/src/context/permissionsContext.tsx
Normal file
120
web/src/context/permissionsContext.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
ReactNode
|
||||||
|
} from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { PermissionResponse } from '../utils/types'
|
||||||
|
import { useModal, useSnackbar } from '../utils/hooks'
|
||||||
|
import { AlertSeverityType } from '../components/snackbar'
|
||||||
|
import usePermissionResponseModal from '../containers/Settings/internal/hooks/usePermissionResponseModal'
|
||||||
|
import { PermissionResponsePayload } from '../containers/Settings/internal/components/permissionResponseModal'
|
||||||
|
|
||||||
|
interface PermissionsContextProps {
|
||||||
|
isLoading: boolean
|
||||||
|
setIsLoading: Dispatch<SetStateAction<boolean>>
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
setPermissions: Dispatch<React.SetStateAction<PermissionResponse[]>>
|
||||||
|
selectedPermission: PermissionResponse | undefined
|
||||||
|
setSelectedPermission: Dispatch<
|
||||||
|
React.SetStateAction<PermissionResponse | undefined>
|
||||||
|
>
|
||||||
|
filteredPermissions: PermissionResponse[]
|
||||||
|
setFilteredPermissions: Dispatch<React.SetStateAction<PermissionResponse[]>>
|
||||||
|
filterApplied: boolean
|
||||||
|
setFilterApplied: Dispatch<SetStateAction<boolean>>
|
||||||
|
fetchPermissions: () => void
|
||||||
|
Dialog: () => JSX.Element
|
||||||
|
setOpenModal: Dispatch<SetStateAction<boolean>>
|
||||||
|
setModalTitle: Dispatch<SetStateAction<string>>
|
||||||
|
setModalPayload: Dispatch<SetStateAction<string>>
|
||||||
|
Snackbar: () => JSX.Element
|
||||||
|
setOpenSnackbar: Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setSnackbarMessage: Dispatch<React.SetStateAction<string>>
|
||||||
|
setSnackbarSeverity: Dispatch<React.SetStateAction<AlertSeverityType>>
|
||||||
|
PermissionResponseDialog: () => JSX.Element
|
||||||
|
setOpenPermissionResponseModal: Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setPermissionResponsePayload: Dispatch<
|
||||||
|
React.SetStateAction<PermissionResponsePayload>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionsContext = createContext<PermissionsContextProps>(
|
||||||
|
undefined!
|
||||||
|
)
|
||||||
|
|
||||||
|
const PermissionsContextProvider = (props: { children: ReactNode }) => {
|
||||||
|
const { children } = props
|
||||||
|
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
|
||||||
|
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||||
|
useSnackbar()
|
||||||
|
const {
|
||||||
|
PermissionResponseDialog,
|
||||||
|
setOpenPermissionResponseModal,
|
||||||
|
setPermissionResponsePayload
|
||||||
|
} = usePermissionResponseModal()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
||||||
|
const [selectedPermission, setSelectedPermission] =
|
||||||
|
useState<PermissionResponse>()
|
||||||
|
const [filteredPermissions, setFilteredPermissions] = useState<
|
||||||
|
PermissionResponse[]
|
||||||
|
>([])
|
||||||
|
const [filterApplied, setFilterApplied] = useState(false)
|
||||||
|
|
||||||
|
const fetchPermissions = useCallback(() => {
|
||||||
|
axios
|
||||||
|
.get(`/SASjsApi/permission`)
|
||||||
|
.then((res: any) => {
|
||||||
|
if (res.data?.length > 0) {
|
||||||
|
setPermissions(res.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PermissionsContext.Provider
|
||||||
|
value={{
|
||||||
|
isLoading,
|
||||||
|
permissions,
|
||||||
|
selectedPermission,
|
||||||
|
filteredPermissions,
|
||||||
|
filterApplied,
|
||||||
|
Dialog,
|
||||||
|
Snackbar,
|
||||||
|
PermissionResponseDialog,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setPermissions,
|
||||||
|
setSelectedPermission,
|
||||||
|
setFilteredPermissions,
|
||||||
|
setFilterApplied,
|
||||||
|
setOpenModal,
|
||||||
|
setModalTitle,
|
||||||
|
setModalPayload,
|
||||||
|
setOpenPermissionResponseModal,
|
||||||
|
setPermissionResponsePayload,
|
||||||
|
setOpenSnackbar,
|
||||||
|
setSnackbarMessage,
|
||||||
|
setSnackbarSeverity
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PermissionsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionsContextProvider
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export * from './useModal'
|
||||||
export * from './usePrompt'
|
export * from './usePrompt'
|
||||||
export * from './useStateWithCallback'
|
export * from './useStateWithCallback'
|
||||||
|
export * from './useSnackbar'
|
||||||
|
|||||||
19
web/src/utils/hooks/useModal.tsx
Normal file
19
web/src/utils/hooks/useModal.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import Modal from '../../components/modal'
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const [openModal, setOpenModal] = useState(false)
|
||||||
|
const [modalTitle, setModalTitle] = useState('')
|
||||||
|
const [modalPayload, setModalPayload] = useState('')
|
||||||
|
|
||||||
|
const Dialog = () => (
|
||||||
|
<Modal
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
title={modalTitle}
|
||||||
|
payload={modalPayload}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { Dialog, setOpenModal, setModalTitle, setModalPayload }
|
||||||
|
}
|
||||||
21
web/src/utils/hooks/useSnackbar.tsx
Normal file
21
web/src/utils/hooks/useSnackbar.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
|
|
||||||
|
export const useSnackbar = () => {
|
||||||
|
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||||
|
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||||
|
AlertSeverityType.Success
|
||||||
|
)
|
||||||
|
|
||||||
|
const Snackbar = () => (
|
||||||
|
<BootstrapSnackbar
|
||||||
|
open={openSnackbar}
|
||||||
|
setOpen={setOpenSnackbar}
|
||||||
|
message={snackbarMessage}
|
||||||
|
severity={snackbarSeverity}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user