1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 11:24:35 +00:00

Compare commits

...

61 Commits

Author SHA1 Message Date
semantic-release-bot
69ddf313b8 chore(release): 0.21.7 [skip ci]
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)

### Bug Fixes

* csrf package is changed to pillarjs-csrf ([fe3e508](fe3e5088f8))
2022-09-30 21:44:16 +00:00
Saad Jutt
65e404cdbd Merge pull request #294 from sasjs/csrf-package-migration
fix: csrf package is changed to pillarjs-csrf
2022-10-01 02:39:06 +05:00
Saad Jutt
fda6ad6356 chore(csrf): removed _csrf completely 2022-09-30 03:07:21 +05:00
Saad Jutt
fe3e5088f8 fix: csrf package is changed to pillarjs-csrf 2022-09-29 20:33:30 +05:00
semantic-release-bot
375f924f45 chore(release): 0.21.6 [skip ci]
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)

### Bug Fixes

* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](40f95f9072))
2022-09-23 09:33:49 +00:00
Allan Bowe
72329e30ed Merge pull request #291 from sasjs/issue-290
fix: in getTokensFromDB handle the scenario when tokens are expired
2022-09-23 10:29:51 +01:00
40f95f9072 fix: in getTokensFromDB handle the scenario when tokens are expired 2022-09-23 09:35:30 +05:00
semantic-release-bot
58e8a869ef chore(release): 0.21.5 [skip ci]
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)

### Bug Fixes

* made files extensions case insensitive ([2496043](249604384e))
2022-09-22 15:50:53 +00:00
Allan Bowe
b558a3d01d Merge pull request #289 from sasjs/issue-288
fix: made files extensions case insensitive
2022-09-22 16:47:00 +01:00
249604384e fix: made files extensions case insensitive 2022-09-22 20:37:16 +05:00
semantic-release-bot
056a436e10 chore(release): 0.21.4 [skip ci]
## [0.21.4](https://github.com/sasjs/server/compare/v0.21.3...v0.21.4) (2022-09-21)

### Bug Fixes

* removing single quotes from _program value ([a0e7875](a0e7875ae6))
2022-09-21 20:02:09 +00:00
Allan Bowe
06d59c618c Merge pull request #287 from sasjs/varfix
fix: removing single quotes from _program value
2022-09-21 20:58:28 +01:00
Allan Bowe
a0e7875ae6 fix: removing single quotes from _program value 2022-09-21 19:57:32 +00:00
semantic-release-bot
24966e695a chore(release): 0.21.3 [skip ci]
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)

### Bug Fixes

* return same tokens if not expired ([330c020](330c020933))
2022-09-21 17:49:49 +00:00
Allan Bowe
5c40d8a342 Merge pull request #286 from sasjs/issue-279
fix: return same tokens if not expired
2022-09-21 18:46:07 +01:00
6f5566dabb chore: lint fix 2022-09-21 22:29:50 +05:00
d93470d183 chore: improve code 2022-09-21 22:27:27 +05:00
330c020933 fix: return same tokens if not expired 2022-09-21 22:12:03 +05:00
munja
a810f6c7cf chore(docs): updating swagger definitions 2022-09-21 11:08:12 +01:00
semantic-release-bot
5d6c6086b4 chore(release): 0.21.2 [skip ci]
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)

### Bug Fixes

* default content-type for sas programs should be text/plain ([9977c9d](9977c9d161))
* **studio:** inject program path to code before sending for execution ([edc2e2a](edc2e2a302))
2022-09-20 21:08:08 +00:00
Allan Bowe
0edcbdcefc Merge pull request #283 from sasjs/fix-default-content-type
fix: default content-type for sas programs should be text/plain
2022-09-20 22:04:27 +01:00
Allan Bowe
ea0222f218 Merge pull request #285 from sasjs/issue-280
fix(studio): inject program path to code before sending for execution
2022-09-20 22:04:16 +01:00
edc2e2a302 fix(studio): inject program path to code before sending for execution 2022-09-21 01:57:01 +05:00
Allan Bowe
efd2e1450e Merge pull request #284 from sasjs/apidocs
chore(docs): updating API docs
2022-09-20 12:25:12 +01:00
munja
1092a73c10 chore(docs): updating API docs 2022-09-20 12:20:50 +01:00
9977c9d161 fix: default content-type for sas programs should be text/plain 2022-09-20 02:32:22 +05:00
semantic-release-bot
5c0eff5197 chore(release): 0.21.1 [skip ci]
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)

### Bug Fixes

* SASJS_WEBOUT_HEADERS path for windows ([0749d65](0749d65173))
2022-09-19 18:58:46 +00:00
Allan Bowe
3bda991a58 Merge pull request #282 from sasjs/issue-281
fix: SASJS_WEBOUT_HEADERS path for windows
2022-09-19 19:54:13 +01:00
0327f7c6ec chore: no need to escapeWinSlash in _sasjs_webout_headers 2022-09-19 23:51:18 +05:00
92549402eb chore: use utility function escapeWinSlashes 2022-09-19 23:36:04 +05:00
semantic-release-bot
b88c911527 chore(release): 0.21.0 [skip ci]
# [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](06d3b17154))
2022-09-19 12:54:27 +00:00
Saad Jutt
8b12f31060 Merge pull request #276 from sasjs/sas9-mock
SAS9 mocker improved - public access denied scenario
2022-09-19 17:50:45 +05:00
Saad Jutt
e65cba9af0 chore: removed deprecated body-parser 2022-09-19 17:47:29 +05:00
0749d65173 fix: SASJS_WEBOUT_HEADERS path for windows 2022-09-19 15:53:51 +05:00
semantic-release-bot
a9c9b734f5 chore(release): 0.20.0 [skip ci]
# [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](d6651bbdbe))
2022-09-16 11:55:57 +00:00
Saad Jutt
39da41c9f1 Merge pull request #277 from sasjs/r-integration
R integration
2022-09-16 16:51:06 +05:00
662b2ca36a chore: replace env variable RSCRIPT_PATH with R_PATH 2022-09-09 15:23:46 +05:00
16b7aa6abb chore: merge js, py and r session controller classes to base session controller class 2022-09-09 00:49:26 +05:00
4560ef942f chore(web): refactor react code 2022-09-08 21:49:35 +05:00
06d3b17154 feat: sas9 mocker improved - public access denied scenario 2022-09-07 18:48:56 +02:00
d6651bbdbe feat: add support for R stored programs 2022-09-06 21:52:21 +05:00
b9d032f148 chore: update swagger.yaml 2022-09-06 21:51:17 +05:00
semantic-release-bot
70655e74d3 chore(release): 0.19.0 [skip ci]
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)

### Features

* added mocking endpoints ([0a0ba2c](0a0ba2cca5))
2022-09-05 12:21:34 +00:00
Allan Bowe
cb82fea0d8 Merge pull request #264 from sasjs/mocker
Mocker
2022-09-05 13:16:10 +01:00
b9a596616d chore: cleanup 2022-09-05 12:20:56 +02:00
semantic-release-bot
72a5393be3 chore(release): 0.18.0 [skip ci]
# [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](ee2db276bb))
2022-09-02 19:26:49 +00:00
Allan Bowe
769a840e9f Merge pull request #273 from sasjs/issue-270
feat: add option for program launch in context menu
2022-09-02 20:23:02 +01:00
730c7c52ac chore: remove commented code 2022-09-03 00:09:48 +05:00
ee2db276bb feat: add option for program launch in context menu 2022-09-02 23:40:02 +05:00
semantic-release-bot
d0a24aacb6 chore(release): 0.17.5 [skip ci]
## [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](393b5eaf99))
2022-09-02 18:08:49 +00:00
Allan Bowe
57dfdf89a4 Merge pull request #272 from sasjs/allanbowe/session-crashed-since-271
fix: SASINITIALFOLDER split over 2 params, closes #271
2022-09-02 19:03:37 +01:00
Allan Bowe
393b5eaf99 fix: SASINITIALFOLDER split over 2 params, closes #271 2022-09-02 17:59:10 +00:00
Saad Jutt
7477326b22 chore: lower cased env values 2022-09-01 23:38:04 +05:00
Saad Jutt
76bf84316e chore: MOCK_SERVERTYPE instead of string literals 2022-09-01 23:34:57 +05:00
7633608318 chore: mocker architecture fix, env validation 2022-08-31 13:31:28 +02:00
572fe22d50 chore: mocksas9 controller 2022-08-30 17:27:37 +02:00
091268bf58 chore: mocking only mandatory bits from sas9 responses 2022-08-29 12:40:29 +02:00
71a4a48443 chore: generic sas9 mock responses 2022-08-29 10:30:01 +02:00
3b188cd724 style: lint 2022-08-26 18:03:28 +02:00
eeba2328c0 chore: added login, logout endpoints 2022-08-26 17:59:07 +02:00
0a0ba2cca5 feat: added mocking endpoints 2022-08-25 15:58:08 +02:00
76 changed files with 4400 additions and 3490 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@ node_modules/
.env*
sas/
sasjs_root/
api/mocks/custom/*
!api/mocks/custom/.keep
tmp/
build/
sasjsbuild/

View File

@@ -1,3 +1,88 @@
## [0.21.7](https://github.com/sasjs/server/compare/v0.21.6...v0.21.7) (2022-09-30)
### Bug Fixes
* csrf package is changed to pillarjs-csrf ([fe3e508](https://github.com/sasjs/server/commit/fe3e5088f8dfff50042ec8e8aac9ba5ba1394deb))
## [0.21.6](https://github.com/sasjs/server/compare/v0.21.5...v0.21.6) (2022-09-23)
### Bug Fixes
* in getTokensFromDB handle the scenario when tokens are expired ([40f95f9](https://github.com/sasjs/server/commit/40f95f9072c8685910138d88fd2410f8704fc975))
## [0.21.5](https://github.com/sasjs/server/compare/v0.21.4...v0.21.5) (2022-09-22)
### Bug Fixes
* made files extensions case insensitive ([2496043](https://github.com/sasjs/server/commit/249604384e42be4c12c88c70a7dff90fc1917a8f))
## [0.21.4](https://github.com/sasjs/server/compare/v0.21.3...v0.21.4) (2022-09-21)
### Bug Fixes
* removing single quotes from _program value ([a0e7875](https://github.com/sasjs/server/commit/a0e7875ae61cbb6e7d3995d2e36e7300b0daec86))
## [0.21.3](https://github.com/sasjs/server/compare/v0.21.2...v0.21.3) (2022-09-21)
### Bug Fixes
* return same tokens if not expired ([330c020](https://github.com/sasjs/server/commit/330c020933f1080261b38f07d6b627f6d7c62446))
## [0.21.2](https://github.com/sasjs/server/compare/v0.21.1...v0.21.2) (2022-09-20)
### Bug Fixes
* default content-type for sas programs should be text/plain ([9977c9d](https://github.com/sasjs/server/commit/9977c9d161947b11d45ab2513f99a5320a3f5a06))
* **studio:** inject program path to code before sending for execution ([edc2e2a](https://github.com/sasjs/server/commit/edc2e2a302ccea4985f3d6b83ef8c23620ab82b6))
## [0.21.1](https://github.com/sasjs/server/compare/v0.21.0...v0.21.1) (2022-09-19)
### Bug Fixes
* SASJS_WEBOUT_HEADERS path for windows ([0749d65](https://github.com/sasjs/server/commit/0749d65173e8cfe9a93464711b7be1e123c289ff))
# [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)

View File

@@ -66,14 +66,14 @@ 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 and js
# 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) and Python (py)
# 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 or PYTHON_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]
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
RUN_TIMES=
# Path to SAS executable (sas.exe / sas.sh)
@@ -85,6 +85,9 @@ 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
# This location is for SAS WORK, staged files, DRIVE, configuration etc
SASJS_ROOT=./sasjs_root
@@ -96,6 +99,9 @@ PROTOCOL=
# default: 5000
PORT=
# options: [sas9|sasviya]
# If not present, mocking function is disabled
MOCK_SERVERTYPE=
#
## Additional SAS Options

View File

@@ -18,6 +18,7 @@ RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
PYTHON_PATH=/usr/bin/python
R_PATH=/usr/bin/Rscript
SASJS_ROOT=./sasjs_root

0
api/mocks/custom/.keep Normal file
View File

View File

@@ -0,0 +1 @@
You have signed in.

View File

@@ -0,0 +1 @@
You have signed out.

View 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>

View File

@@ -0,0 +1 @@
Public access has been denied.

View File

@@ -0,0 +1 @@
"title": "Log Off SAS Demo User"

111
api/package-lock.json generated
View File

@@ -9,12 +9,11 @@
"version": "0.0.2",
"dependencies": {
"@sasjs/core": "^4.31.3",
"@sasjs/utils": "2.42.1",
"@sasjs/utils": "2.48.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"helmet": "^5.0.2",
@@ -37,7 +36,6 @@
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
@@ -50,6 +48,7 @@
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"csrf": "^3.1.0",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",
@@ -1396,9 +1395,9 @@
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
},
"node_modules/@sasjs/utils": {
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"version": "2.48.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
"hasInstallScript": true,
"dependencies": {
"@types/fs-extra": "9.0.13",
@@ -1833,15 +1832,6 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"node_modules/@types/csurf": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
"dev": true,
"dependencies": {
"@types/express-serve-static-core": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
@@ -3336,6 +3326,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"dev": true,
"dependencies": {
"rndm": "1.2.0",
"tsscmp": "1.0.6",
@@ -3369,40 +3360,6 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
"dev": true
},
"node_modules/csurf": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
"dependencies": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"csrf": "3.1.0",
"http-errors": "~1.7.3"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/csurf/node_modules/http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/csurf/node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/csv-stringify": {
"version": "5.6.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
@@ -8377,7 +8334,8 @@
"node_modules/rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=",
"dev": true
},
"node_modules/rotating-file-stream": {
"version": "3.0.4",
@@ -9389,6 +9347,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"dev": true,
"engines": {
"node": ">=0.6.x"
}
@@ -10974,9 +10933,9 @@
"integrity": "sha512-TpVqWl5bqp3JTQjIg0r4WiQg7Ima5f17eAJILJbdYDdXsnLXlA/Csbb95G7eDPhzWpM3C0NrzKek3yvCMGzXIA=="
},
"@sasjs/utils": {
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"version": "2.48.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.48.1.tgz",
"integrity": "sha512-Eu9p66JKLeTj0KK3kfY7YLQYq+MDMS1Q1/FOFfRe9hV23mFsuzierVMrnEYGK0JaHOogdHLmwzg6iVLDT8Jssg==",
"requires": {
"@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13",
@@ -11361,15 +11320,6 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"@types/csurf": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
"integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*"
}
},
"@types/express": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz",
@@ -12584,6 +12534,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"dev": true,
"requires": {
"rndm": "1.2.0",
"tsscmp": "1.0.6",
@@ -12613,36 +12564,6 @@
}
}
},
"csurf": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"csrf": "3.1.0",
"http-errors": "~1.7.3"
},
"dependencies": {
"http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}
}
},
"csv-stringify": {
"version": "5.6.5",
"resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz",
@@ -16379,7 +16300,8 @@
"rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=",
"dev": true
},
"rotating-file-stream": {
"version": "3.0.4",
@@ -17134,7 +17056,8 @@
"tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
"dev": true
},
"tunnel-agent": {
"version": "0.6.0",

View File

@@ -48,12 +48,11 @@
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "^4.31.3",
"@sasjs/utils": "2.42.1",
"@sasjs/utils": "2.48.1",
"bcryptjs": "^2.4.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csurf": "^1.11.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"helmet": "^5.0.2",
@@ -73,7 +72,6 @@
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/csurf": "^1.11.2",
"@types/express": "^4.17.12",
"@types/express-session": "^1.17.4",
"@types/jest": "^26.0.24",
@@ -86,6 +84,7 @@
"@types/swagger-ui-express": "^4.1.3",
"@types/unzipper": "^0.10.5",
"adm-zip": "^0.5.9",
"csrf": "^3.1.0",
"dotenv": "^10.0.0",
"http-headers-validation": "^0.0.1",
"jest": "^27.0.6",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import path from 'path'
import express, { ErrorRequestHandler } from 'express'
import csrf, { CookieOptions } from 'csurf'
import express, { ErrorRequestHandler, CookieOptions } from 'express'
import cookieParser from 'cookie-parser'
import dotenv from 'dotenv'
@@ -39,15 +38,7 @@ export const cookieOptions: CookieOptions = {
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
/***********************************
* CSRF Protection *
***********************************/
export const csrfProtection = csrf({ cookie: cookieOptions })
const onError: ErrorRequestHandler = (err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN')
return res.status(400).send('Invalid CSRF token!')
console.error(err.stack)
res.status(500).send('Something broke!')
}
@@ -77,6 +68,10 @@ export default setProcessVariables().then(async () => {
app.use(express.json({ limit: '100mb' }))
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 copySASjsCore()

View File

@@ -4,6 +4,7 @@ import { InfoJWT } from '../types'
import {
generateAccessToken,
generateRefreshToken,
getTokensFromDB,
removeTokensInDB,
saveTokensInDB
} from '../utils'
@@ -73,6 +74,15 @@ const token = async (data: any): Promise<TokenResponse> => {
AuthController.deleteCode(userInfo.userId, clientId)
// get tokens from DB
const existingTokens = await getTokensFromDB(userInfo.userId, clientId)
if (existingTokens) {
return {
accessToken: existingTokens.accessToken,
refreshToken: existingTokens.refreshToken
}
}
const accessToken = generateAccessToken(userInfo)
const refreshToken = generateRefreshToken(userInfo)

View File

@@ -24,11 +24,11 @@ interface ExecuteCodePayload {
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
@Tags('Code')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
* Execute Code on the Specified Runtime
* @summary Run Code and Return Webout Content and Log
*/
@Post('/execute')
public async executeCode(

View File

@@ -19,13 +19,41 @@ import {
const execFilePromise = promisify(execFile)
abstract class SessionController {
export class SessionController {
protected sessions: Session[] = []
protected getReadySessions = (): Session[] =>
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() {
const readySessions = this.getReadySessions()
@@ -64,6 +92,9 @@ export class SASSessionController extends SessionController {
path: sessionFolder
}
const headersPath = path.join(session.path, 'stpsrv_header.txt')
await createFile(headersPath, 'Content-type: text/plain')
// we do not want to leave sessions running forever
// we clean them up after a predefined period, if unused
this.scheduleSessionDestroy(session)
@@ -101,15 +132,14 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
isWindows() ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
process.sasLoc!.endsWith('sas.exe')
? '-SASINITIALFOLDER ' + session.path
: '',
isWindows() ? '-nologo' : ''
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
])
.then(() => {
session.completed = true
@@ -143,7 +173,7 @@ ${autoExecContent}`
session.ready = true
}
public async deleteSession(session: Session) {
private async deleteSession(session: Session) {
// remove the temporary files, to avoid buildup
await deleteFolder(session.path)
@@ -168,110 +198,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: text/plain')
this.sessions.push(session)
return session
}
}
export class PythonSessionController 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: text/plain')
this.sessions.push(session)
return session
}
}
export const getSessionController = (
runTime: RunTimeType
): SASSessionController | JSSessionController | PythonSessionController => {
if (runTime === RunTimeType.SAS) {
return getSASSessionController()
}
): SessionController => {
if (process.sessionController) return process.sessionController
if (runTime === RunTimeType.JS) {
return getJSSessionController()
}
process.sessionController =
runTime === RunTimeType.SAS
? new SASSessionController()
: new SessionController()
if (runTime === RunTimeType.PY) {
return getPythonSessionController()
}
throw new Error('No Runtime is configured')
}
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 getPythonSessionController = (): PythonSessionController => {
if (process.pythonSessionController) return process.pythonSessionController
process.pythonSessionController = new PythonSessionController()
return process.pythonSessionController
return process.sessionController
}
const autoExecContent = `

View File

@@ -1,4 +1,4 @@
import { isWindows } from '@sasjs/utils'
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadJSCode } from '../../utils'
import { ExecutionVars } from './'
@@ -21,13 +21,9 @@ export const createJSProgram = async (
const preProgramVarStatments = `
let _webout = '';
const weboutPath = '${
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
}';
const _SASJS_TOKENFILE = '${
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
}';
const _SASJS_WEBOUT_HEADERS = '${headersPath}';
const weboutPath = '${escapeWinSlashes(weboutPath)}';
const _SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
const _SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
const _SASJS_USERNAME = '${preProgramVariables?.username}';
const _SASJS_USERID = '${preProgramVariables?.userId}';
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';

View File

@@ -1,4 +1,4 @@
import { isWindows } from '@sasjs/utils'
import { escapeWinSlashes } from '@sasjs/utils'
import { PreProgramVars, Session } from '../../types'
import { generateFileUploadPythonCode } from '../../utils'
import { ExecutionVars } from './'
@@ -19,14 +19,10 @@ export const createPythonProgram = async (
)
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_SESSION_PATH = '${escapeWinSlashes(session.path)}';
_WEBOUT = '${escapeWinSlashes(weboutPath)}';
_SASJS_WEBOUT_HEADERS = '${escapeWinSlashes(headersPath)}';
_SASJS_TOKENFILE = '${escapeWinSlashes(tokenFile)}';
_SASJS_USERNAME = '${preProgramVariables?.username}';
_SASJS_USERID = '${preProgramVariables?.userId}';
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';

View File

@@ -0,0 +1,64 @@
import { escapeWinSlashes } 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 <- '${escapeWinSlashes(session.path)}';
._WEBOUT <- '${escapeWinSlashes(weboutPath)}';
._SASJS_WEBOUT_HEADERS <- '${escapeWinSlashes(headersPath)}';
._SASJS_TOKENFILE <- '${escapeWinSlashes(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
}

View File

@@ -8,6 +8,7 @@ export const createSASProgram = async (
vars: ExecutionVars,
session: Session,
weboutPath: string,
headersPath: string,
tokenFile: string,
otherArgs?: any
) => {
@@ -23,7 +24,7 @@ export const createSASProgram = async (
%let _sasjs_displayname=${preProgramVariables?.displayName};
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
%let _sasjs_apipath=/SASjsApi/stp/execute;
%let _sasjs_webout_headers=%sysfunc(pathname(work))/../stpsrv_header.txt;
%let _sasjs_webout_headers=${headersPath};
%let _metaperson=&_sasjs_displayname;
%let _metauser=&_sasjs_username;

View File

@@ -5,4 +5,5 @@ export * from './FileUploadController'
export * from './createSASProgram'
export * from './createJSProgram'
export * from './createPythonProgram'
export * from './createRProgram'
export * from './processProgram'

View File

@@ -9,7 +9,8 @@ import {
ExecutionVars,
createSASProgram,
createJSProgram,
createPythonProgram
createPythonProgram,
createRProgram
} from './'
export const processProgram = async (
@@ -24,87 +25,14 @@ export const processProgram = async (
logPath: string,
otherArgs?: any
) => {
if (runTime === RunTimeType.JS) {
program = await createJSProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
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 if (runTime === RunTimeType.PY) {
program = await createPythonProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
const codePath = path.join(session.path, 'code.py')
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.pythonLoc!, [codePath], {
stdio: ['ignore', writeStream, writeStream]
})
// copy the code.py 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 {
if (runTime === RunTimeType.SAS) {
program = await createSASProgram(
program,
preProgramVariables,
vars,
session,
weboutPath,
headersPath,
tokenFile,
otherArgs
)
@@ -124,6 +52,82 @@ export const processProgram = async (
while (!session.completed) {
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)
}
}
}

View 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
}
}

View File

@@ -23,14 +23,14 @@ interface ExecutePostRequestPayload {
@Tags('STP')
export class STPController {
/**
* Trigger a SAS or JS program using the _program URL parameter.
* Trigger a Stored Program using the _program URL parameter.
*
* Accepts URL parameters and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* @summary Execute a Stored Program, returns raw _webout content.
* @param _program Location of SAS or JS code
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive
* @example _program "/Projects/myApp/some/program"
*/
@Get('/execute')
@@ -43,21 +43,15 @@ export class STPController {
}
/**
* Trigger a SAS or JS program using the _program URL parameter.
* Trigger a Stored Program using the _program URL parameter.
*
* Accepts URL parameters and file uploads. For more details, see docs:
*
* https://server.sasjs.io/storedprograms
*
* The response will be a JSON object with the following root attributes:
* log, webout, headers.
*
* The webout attribute will be nested JSON ONLY if the response-header
* contains a content-type of application/json AND it is valid JSON.
* Otherwise it will be a stringified version of the webout content.
*
* @summary Execute a Stored Program, return a JSON object
* @param _program Location of SAS or JS code
* @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive
* @example _program "/Projects/myApp/some/program"
*/
@Post('/execute')

View File

@@ -1,6 +1,6 @@
import { RequestHandler, Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { csrfProtection } from '../app'
import { csrfProtection } from './'
import {
fetchLatestAutoExec,
ModeType,

View File

@@ -10,9 +10,7 @@ import { getPath, isPublicRoute } from '../utils'
export const authorize: RequestHandler = async (req, res, next) => {
const { user } = req
if (!user) {
return res.sendStatus(401)
}
if (!user) return res.sendStatus(401)
// no need to check for permissions when user is admin
if (user.isAdmin) return next()

View File

@@ -0,0 +1,32 @@
import { RequestHandler } from 'express'
import csrf from 'csrf'
const csrfTokens = new csrf()
const secret = csrfTokens.secretSync()
export const generateCSRFToken = () => csrfTokens.create(secret)
export const csrfProtection: RequestHandler = (req, res, next) => {
if (req.method === 'GET') return next()
// Reads the token from the following locations, in order:
// req.body.csrf_token - typically generated by the body-parser module.
// req.query.csrf_token - a built-in from Express.js to read from the URL query string.
// req.headers['csrf-token'] - the CSRF-Token HTTP request header.
// req.headers['xsrf-token'] - the XSRF-Token HTTP request header.
// req.headers['x-csrf-token'] - the X-CSRF-Token HTTP request header.
// req.headers['x-xsrf-token'] - the X-XSRF-Token HTTP request header.
const token =
req.body?.csrf_token ||
req.query?.csrf_token ||
req.headers['csrf-token'] ||
req.headers['xsrf-token'] ||
req.headers['x-csrf-token'] ||
req.headers['x-xsrf-token']
if (!csrfTokens.verify(secret, token)) {
return res.status(400).send('Invalid CSRF token!')
}
next()
}

View File

@@ -1,5 +1,6 @@
export * from './authenticateToken'
export * from './authorize'
export * from './csrfProtection'
export * from './desktop'
export * from './verifyAdmin'
export * from './verifyAdminIfNeeded'
export * from './authorize'

View File

@@ -7,7 +7,7 @@ import {
authenticateRefreshToken
} from '../../middlewares'
import { authorizeValidation, tokenValidation } from '../../utils'
import { tokenValidation } from '../../utils'
import { InfoJWT } from '../../types'
const authRouter = express.Router()

View File

@@ -21,9 +21,8 @@ import {
} from '../../../utils'
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
import {
SASSessionController,
JSSessionController,
PythonSessionController
SessionController,
SASSessionController
} from '../../../controllers/internal'
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
import { Session } from '../../../types'
@@ -472,11 +471,7 @@ const setupMocks = async () => {
.mockImplementation(mockedGetSession)
jest
.spyOn(JSSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest
.spyOn(PythonSessionController.prototype, 'getSession')
.spyOn(SASSessionController.prototype, 'getSession')
.mockImplementation(mockedGetSession)
jest

View File

@@ -49,10 +49,9 @@ describe('web', () => {
describe('SASLogon/login', () => {
let csrfToken: string
let cookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
;({ csrfToken } = await getCSRF(app))
})
afterEach(async () => {
@@ -66,7 +65,6 @@ describe('web', () => {
const res = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send({
username: user.username,
@@ -82,15 +80,45 @@ describe('web', () => {
isAdmin: user.isAdmin
})
})
it('should respond with Bad Request if CSRF Token is not present', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if CSRF Token is invalid', async () => {
await userController.createUser(user)
const res = await request(app)
.post('/SASLogon/login')
.set('x-xsrf-token', 'INVALID_CSRF_TOKEN')
.send({
username: user.username,
password: user.password
})
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
})
describe('SASLogon/authorize', () => {
let csrfToken: string
let cookies: string
let authCookies: string
beforeAll(async () => {
;({ csrfToken, cookies } = await getCSRF(app))
;({ csrfToken } = await getCSRF(app))
await userController.createUser(user)
@@ -99,12 +127,7 @@ describe('web', () => {
password: user.password
}
;({ cookies: authCookies } = await performLogin(
app,
credentials,
cookies,
csrfToken
))
;({ authCookies } = await performLogin(app, credentials, csrfToken))
})
afterAll(async () => {
@@ -116,17 +139,28 @@ describe('web', () => {
it('should respond with authorization code', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({ clientId })
expect(res.body).toHaveProperty('code')
})
it('should respond with Bad Request if CSRF Token is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies].join('; '))
.send({ clientId })
.expect(400)
expect(res.text).toEqual('Invalid CSRF token!')
expect(res.body).toEqual({})
})
it('should respond with Bad Request if clientId is missing', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({})
.expect(400)
@@ -138,7 +172,7 @@ describe('web', () => {
it('should respond with Forbidden if clientId is incorrect', async () => {
const res = await request(app)
.post('/SASLogon/authorize')
.set('Cookie', [authCookies, cookies].join('; '))
.set('Cookie', [authCookies].join('; '))
.set('x-xsrf-token', csrfToken)
.send({
clientId: 'WrongClientID'
@@ -153,27 +187,22 @@ describe('web', () => {
const getCSRF = async (app: Express) => {
// make request to get CSRF
const { header, text } = await request(app).get('/')
const cookies = header['set-cookie'].join()
const { text } = await request(app).get('/')
const csrfToken = extractCSRF(text)
return { csrfToken, cookies }
return { csrfToken: extractCSRF(text) }
}
const performLogin = async (
app: Express,
credentials: { username: string; password: string },
cookies: string,
csrfToken: string
) => {
const { header } = await request(app)
.post('/SASLogon/login')
.set('Cookie', cookies)
.set('x-xsrf-token', csrfToken)
.send(credentials)
const newCookies: string = header['set-cookie'].join()
return { cookies: newCookies }
return { authCookies: header['set-cookie'].join() }
}
const extractCSRF = (text: string) =>

View File

@@ -1,6 +1,6 @@
import path from 'path'
import express, { Request } from 'express'
import { authenticateAccessToken } from '../../middlewares'
import { authenticateAccessToken, generateCSRFToken } from '../../middlewares'
import { folderExists } from '@sasjs/utils'
import { addEntryToAppStreamConfig, getFilesFolder } from '../../utils'
@@ -13,7 +13,7 @@ const router = express.Router()
router.get('/', authenticateAccessToken, async (req, res) => {
const content = appStreamHtml(process.appStreamConfig)
res.cookie('XSRF-TOKEN', req.csrfToken())
res.cookie('XSRF-TOKEN', generateCSRFToken())
return res.send(content)
})

View File

@@ -4,7 +4,7 @@ import webRouter from './web'
import apiRouter from './api'
import appStreamRouter from './appStream'
import { csrfProtection } from '../app'
import { csrfProtection } from '../middlewares'
export const setupRoutes = (app: Express) => {
app.use('/SASjsApi', apiRouter)

View File

@@ -1,8 +1,25 @@
import express from 'express'
import sas9WebRouter from './sas9-web'
import sasViyaWebRouter from './sasviya-web'
import webRouter from './web'
import { MOCK_SERVERTYPEType } from '../../utils'
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

View File

@@ -0,0 +1,119 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
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=${generateCSRFToken()}; 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

View File

@@ -0,0 +1,33 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
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=${generateCSRFToken()}; 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

View File

@@ -1,4 +1,5 @@
import express from 'express'
import { generateCSRFToken } from '../../middlewares'
import { WebController } from '../../controllers/web'
import { authenticateAccessToken, desktopRestrict } from '../../middlewares'
import { authorizeValidation, loginWebValidation } from '../../utils'
@@ -13,7 +14,7 @@ webRouter.get('/', async (req, res) => {
} 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 codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${generateCSRFToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`

View File

@@ -3,12 +3,11 @@ declare namespace NodeJS {
sasLoc?: string
nodeLoc?: string
pythonLoc?: string
rLoc?: string
driveLoc: string
logsLoc: string
logsUUID: string
sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
pythonSessionController?: import('../../controllers/internal').PythonSessionController
sessionController?: import('../../controllers/internal').SessionController
appStreamConfig: import('../').AppStreamConfig
logger: import('@sasjs/utils/logger').Logger
runTimes: import('../../utils').RunTimeType[]

View File

@@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
import { RunTimeType } from './verifyEnvVariables'
export const getDesktopFields = async () => {
const { SAS_PATH, NODE_PATH, PYTHON_PATH } = process.env
const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env
let sasLoc, nodeLoc, pythonLoc
let sasLoc, nodeLoc, pythonLoc, rLoc
if (process.runTimes.includes(RunTimeType.SAS)) {
sasLoc = SAS_PATH ?? (await getSASLocation())
@@ -20,7 +20,11 @@ export const getDesktopFields = async () => {
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
}
return { sasLoc, nodeLoc, pythonLoc }
if (process.runTimes.includes(RunTimeType.R)) {
rLoc = R_PATH ?? (await getRLocation())
}
return { sasLoc, nodeLoc, pythonLoc, rLoc }
}
const getDriveLocation = async (): Promise<string> => {
@@ -117,3 +121,25 @@ const getPythonLocation = async (): Promise<string> => {
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
}

View File

@@ -7,7 +7,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const { user, accessToken } = req
const csrfToken = req.headers['x-xsrf-token'] || req.cookies['XSRF-TOKEN']
const sessionId = req.cookies['connect.sid']
const { _csrf } = req.cookies
const httpHeaders: string[] = []
@@ -16,7 +15,6 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
const cookies: string[] = []
if (sessionId) cookies.push(`connect.sid=${sessionId}`)
if (_csrf) cookies.push(`_csrf=${_csrf}`)
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)

View File

@@ -4,8 +4,8 @@ import { getFilesFolder } from './file'
import { RunTimeType } from '.'
export const getRunTimeAndFilePath = async (programPath: string) => {
const ext = path.extname(programPath)
// If programPath (_program) is provided with a ".sas", ".js" or ".py" extension
const ext = path.extname(programPath).toLowerCase()
// If programPath (_program) is provided with a ".sas", ".js", ".py" or ".r" extension
// we should use that extension to determine the appropriate runTime
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
const runTime = ext.slice(1)

View File

@@ -0,0 +1,55 @@
import jwt from 'jsonwebtoken'
import User from '../model/User'
const isValidToken = async (
token: string,
key: string,
userId: number,
clientId: string
) => {
const promise = new Promise<boolean>((resolve, reject) =>
jwt.verify(token, key, (err, decoded) => {
if (err) return reject(false)
if (decoded?.userId === userId && decoded?.clientId === clientId) {
return resolve(true)
}
return reject(false)
})
)
return await promise.then(() => true).catch(() => false)
}
export const getTokensFromDB = async (userId: number, clientId: string) => {
const user = await User.findOne({ id: userId })
if (!user) return
const currentTokenObj = user.tokens.find(
(tokenObj: any) => tokenObj.clientId === clientId
)
if (currentTokenObj) {
const accessToken = currentTokenObj.accessToken
const refreshToken = currentTokenObj.refreshToken
const isValidAccessToken = await isValidToken(
accessToken,
process.secrets.ACCESS_TOKEN_SECRET,
userId,
clientId
)
const isValidRefreshToken = await isValidToken(
refreshToken,
process.secrets.REFRESH_TOKEN_SECRET,
userId,
clientId
)
if (isValidAccessToken && isValidRefreshToken) {
return { accessToken, refreshToken }
}
}
}

View File

@@ -14,6 +14,7 @@ export * from './getDesktopFields'
export * from './getPreProgramVariables'
export * from './getRunTimeAndFilePath'
export * from './getServerUrl'
export * from './getTokensFromDB'
export * from './instantiateLogger'
export * from './isDebugOn'
export * from './isPublicRoute'

View File

@@ -29,12 +29,14 @@ export const setProcessVariables = async () => {
process.sasLoc = process.env.SAS_PATH
process.nodeLoc = process.env.NODE_PATH
process.pythonLoc = process.env.PYTHON_PATH
process.rLoc = process.env.R_PATH
} else {
const { sasLoc, nodeLoc, pythonLoc } = await getDesktopFields()
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
process.sasLoc = sasLoc
process.nodeLoc = nodeLoc
process.pythonLoc = pythonLoc
process.rLoc = rLoc
}
const { SASJS_ROOT } = process.env

View File

@@ -157,3 +157,30 @@ export const generateFileUploadPythonCode = async (
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
}

View File

@@ -1,3 +1,8 @@
export enum MOCK_SERVERTYPEType {
SAS9 = 'sas9',
SASVIYA = 'sasviya'
}
export enum ModeType {
Server = 'server',
Desktop = 'desktop'
@@ -29,7 +34,8 @@ export enum LOG_FORMAT_MORGANType {
export enum RunTimeType {
SAS = 'sas',
JS = 'js',
PY = 'py'
PY = 'py',
R = 'r'
}
export enum ReturnCode {
@@ -40,6 +46,8 @@ export enum ReturnCode {
export const verifyEnvVariables = (): ReturnCode => {
const errors: string[] = []
errors.push(...verifyMOCK_SERVERTYPE())
errors.push(...verifyMODE())
errors.push(...verifyPROTOCOL())
@@ -66,6 +74,23 @@ export const verifyEnvVariables = (): ReturnCode => {
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 errors: string[] = []
const { MODE } = process.env
@@ -229,7 +254,8 @@ const verifyRUN_TIMES = (): string[] => {
const verifyExecutablePaths = () => {
const errors: string[] = []
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, MODE } = process.env
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
process.env
if (MODE === ModeType.Server) {
const runTimes = RUN_TIMES?.split(',')
@@ -245,6 +271,10 @@ const verifyExecutablePaths = () => {
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

View File

@@ -20,7 +20,7 @@
"description": "Operations about clients"
},
{
"name": "CODE",
"name": "Code",
"description": "Execution of code (various runtimes are supported)"
},
{

View File

@@ -78,6 +78,18 @@ const TreeViewNode = ({
mouseY: number
} | 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) => {
event.preventDefault()
event.stopPropagation()
@@ -224,8 +236,8 @@ const TreeViewNode = ({
: undefined
}
>
{node.isFolder && (
<div>
{node.isFolder ? (
<>
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
<MenuItem
disabled={!node.relativePath}
@@ -233,14 +245,21 @@ const TreeViewNode = ({
>
Add File
</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>
</div>
)

View File

@@ -9,6 +9,7 @@ import Permission from './permission'
import Profile from './profile'
import { AppContext, ModeType } from '../../context/appContext'
import PermissionsContextProvider from '../../context/permissionsContext'
const StyledTab = styled(Tab)({
background: 'black',
@@ -64,7 +65,9 @@ const Settings = () => {
<Profile />
</StyledTabpanel>
<StyledTabpanel value="permission">
<Permission />
<PermissionsContextProvider>
<Permission />
</PermissionsContextProvider>
</StyledTabpanel>
</TabContext>
</Box>

View File

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

View File

@@ -3,31 +3,21 @@ import axios from 'axios'
import {
Button,
Grid,
Dialog,
DialogContent,
DialogActions,
TextField,
CircularProgress,
Autocomplete
} 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 {
UserResponse,
GroupResponse,
RegisterPermissionPayload
} from '../../utils/types'
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialogContent-root': {
padding: theme.spacing(2)
},
'& .MuiDialogActions-root': {
padding: theme.spacing(1)
}
}))
} from '../../../../utils/types'
type AddPermissionModalProps = {
open: boolean

View File

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

View File

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

View File

@@ -10,9 +10,9 @@ import {
import { styled } from '@mui/material/styles'
import Autocomplete from '@mui/material/Autocomplete'
import { PermissionResponse } from '../../utils/types'
import { BootstrapDialogTitle } from '../../components/dialogTitle'
import { PrincipalType } from './permission'
import { PermissionResponse } from '../../../../utils/types'
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
import { PrincipalType } from '../hooks/usePermission'
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialogContent-root': {

View File

@@ -2,9 +2,9 @@ import React from 'react'
import { Typography, DialogContent } from '@mui/material'
import { BootstrapDialog } from '../../components/modal'
import { BootstrapDialogTitle } from '../../components/dialogTitle'
import { PermissionResponse } from '../../utils/types'
import { BootstrapDialog } from '../../../../components/modal'
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
import { PermissionResponse } from '../../../../utils/types'
export interface PermissionResponsePayload {
permissionType: string

View File

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

View File

@@ -2,26 +2,17 @@ import React, { useState, Dispatch, SetStateAction, useEffect } from 'react'
import {
Button,
Grid,
Dialog,
DialogContent,
DialogActions,
TextField
} from '@mui/material'
import { styled } from '@mui/material/styles'
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'
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialogContent-root': {
padding: theme.spacing(2)
},
'& .MuiDialogActions-root': {
padding: theme.spacing(1)
}
}))
import { PermissionResponse } from '../../../../utils/types'
type UpdatePermissionModalProps = {
open: boolean

View 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
}

View 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

View File

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

View File

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

View 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

View File

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

View File

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

View File

@@ -1,54 +1,7 @@
import React, { useState, useEffect, useContext, useCallback } from 'react'
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 { Box, Paper, Grid, CircularProgress } from '@mui/material'
import { styled } from '@mui/material/styles'
import Modal from '../../components/modal'
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'
})
import PermissionTable from './internal/components/permissionTable'
import usePermission from './internal/hooks/usePermission'
const BootstrapGridItem = styled(Grid)({
'&.MuiGrid-item': {
@@ -56,298 +9,23 @@ const BootstrapGridItem = styled(Grid)({
}
})
export enum PrincipalType {
User = 'User',
Group = 'Group'
}
const Permission = () => {
const appContext = useContext(AppContext)
const [isLoading, setIsLoading] = useState(false)
const [openModal, setOpenModal] = useState(false)
const [modalTitle, setModalTitle] = useState('')
const [modalPayload, setModalPayload] = useState('')
const [openSnackbar, setOpenSnackbar] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
AlertSeverityType.Success
)
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
useState(false)
const [permissionResponsePayload, setPermissionResponsePayload] =
useState<PermissionResponsePayload>({
permissionType: '',
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)
})
}
const {
filterApplied,
filteredPermissions,
isAdmin,
isLoading,
permissions,
AddPermissionButton,
UpdatePermissionDialog,
DeletePermissionDialog,
FilterPermissionsButton,
handleDeletePermissionClick,
handleUpdatePermissionClick,
PermissionResponseDialog,
Dialog,
Snackbar
} = usePermission()
return isLoading ? (
<CircularProgress
@@ -358,22 +36,8 @@ const Permission = () => {
<Grid container direction="column" spacing={1}>
<BootstrapGridItem item xs={12}>
<Paper elevation={3} sx={{ display: 'flex' }}>
<Tooltip title="Filter Permissions">
<IconButton onClick={() => setFilterModalOpen(true)}>
<FilterListIcon />
</IconButton>
</Tooltip>
{appContext.isAdmin && (
<Tooltip
sx={{ marginLeft: 'auto' }}
title="Add Permission"
placement="bottom-end"
>
<IconButton onClick={() => setAddPermissionModalOpen(true)}>
<AddIcon />
</IconButton>
</Tooltip>
)}
<FilterPermissionsButton />
{isAdmin && <AddPermissionButton />}
</Paper>
</BootstrapGridItem>
<BootstrapGridItem item xs={12}>
@@ -384,192 +48,13 @@ const Permission = () => {
/>
</BootstrapGridItem>
</Grid>
<BootstrapSnackbar
open={openSnackbar}
setOpen={setOpenSnackbar}
message={snackbarMessage}
severity={snackbarSeverity}
/>
<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}
/>
<PermissionResponseDialog />
<UpdatePermissionDialog />
<DeletePermissionDialog />
<Dialog />
<Snackbar />
</Box>
)
}
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
}

View File

@@ -1,55 +1,26 @@
import React, {
Dispatch,
SetStateAction,
useEffect,
useRef,
useState,
useContext,
useCallback
} from 'react'
import axios from 'axios'
import React, { Dispatch, SetStateAction } from 'react'
import {
Backdrop,
Box,
Button,
CircularProgress,
FormControl,
IconButton,
Menu,
MenuItem,
Paper,
Select,
SelectChangeEvent,
Tab,
Tooltip,
Typography
} from '@mui/material'
import { styled } from '@mui/material/styles'
import {
RocketLaunch,
MoreVert,
Save,
SaveAs,
Difference,
Edit
} from '@mui/icons-material'
import Editor, {
MonacoDiffEditor,
DiffEditorDidMount,
EditorDidMount,
monaco
} from 'react-monaco-editor'
import Editor, { MonacoDiffEditor } from 'react-monaco-editor'
import { TabContext, TabList, TabPanel } from '@mui/lab'
import { AppContext, RunTimeType } from '../../context/appContext'
import FilePathInputModal from '../../components/filePathInputModal'
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
import Modal from '../../components/modal'
import FileMenu from './internal/components/fileMenu'
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)(() => ({
padding: '10px'
@@ -70,267 +41,77 @@ type SASjsEditorProps = {
setTab: Dispatch<SetStateAction<string>>
}
const baseUrl = window.location.origin
const SASJS_LOGS_SEPARATOR =
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
const SASjsEditor = ({
selectedFilePath,
setSelectedFilePath,
tab,
setTab
}: SASjsEditorProps) => {
const appContext = useContext(AppContext)
const [isLoading, setIsLoading] = useState(false)
const [openModal, setOpenModal] = useState(false)
const [modalTitle, setModalTitle] = useState('')
const [modalPayload, setModalPayload] = useState('')
const [openSnackbar, setOpenSnackbar] = useState(false)
const [snackbarMessage, setSnackbarMessage] = useState('')
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
AlertSeverityType.Success
)
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 {
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
} = useEditor({ selectedFilePath, setSelectedFilePath, setTab })
usePrompt(
'Changes you made may not be saved.',
prevFileContent !== fileContent && !!selectedFilePath
)
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)
})
},
[
fileContent,
prevFileContent,
selectedFilePath,
setPrevFileContent,
setSelectedFilePath
]
const fileMenu = (
<FileMenu
showDiff={showDiff}
setShowDiff={setShowDiff}
prevFileContent={prevFileContent}
currentFileContent={fileContent}
selectedFilePath={selectedFilePath}
setOpenFilePathInputModal={setOpenFilePathInputModal}
saveFile={saveFile}
/>
)
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])
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) => {
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() || 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 monacoEditor = showDiff ? (
<MonacoDiffEditor
height="98%"
language={getLanguageFromExtension(selectedFileExtension)}
original={prevFileContent}
value={fileContent}
editorDidMount={handleDiffEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
) : (
<Editor
height="98%"
language={getLanguageFromExtension(selectedFileExtension)}
value={fileContent}
editorDidMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => setFileContent(val)}
/>
)
return (
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
@@ -343,15 +124,7 @@ const SASjsEditor = ({
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
<Box sx={{ marginTop: '10px' }}>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<FileMenu
showDiff={showDiff}
setShowDiff={setShowDiff}
prevFileContent={prevFileContent}
currentFileContent={fileContent}
selectedFilePath={selectedFilePath}
setOpenFilePathInputModal={setOpenFilePathInputModal}
saveFile={saveFile}
/>
{fileMenu}
</Box>
<Paper
sx={{
@@ -363,26 +136,7 @@ const SASjsEditor = ({
}}
elevation={3}
>
{showDiff ? (
<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)}
/>
)}
{monacoEditor}
</Paper>
</Box>
) : (
@@ -419,15 +173,7 @@ const SASjsEditor = ({
handleChangeRunTime={handleChangeRunTime}
handleRunBtnClick={handleRunBtnClick}
/>
<FileMenu
showDiff={showDiff}
setShowDiff={setShowDiff}
prevFileContent={prevFileContent}
currentFileContent={fileContent}
selectedFilePath={selectedFilePath}
setOpenFilePathInputModal={setOpenFilePathInputModal}
saveFile={saveFile}
/>
{fileMenu}
</Box>
<Paper
onKeyUp={handleKeyUp}
@@ -440,26 +186,7 @@ const SASjsEditor = ({
}}
elevation={3}
>
{showDiff ? (
<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)}
/>
)}
{monacoEditor}
<p
style={{
position: 'absolute',
@@ -489,18 +216,8 @@ const SASjsEditor = ({
</StyledTabPanel>
</TabContext>
)}
<Modal
open={openModal}
setOpen={setOpenModal}
title={modalTitle}
payload={modalPayload}
/>
<BootstrapSnackbar
open={openSnackbar}
setOpen={setOpenSnackbar}
message={snackbarMessage}
severity={snackbarSeverity}
/>
<Dialog />
<Snackbar />
<FilePathInputModal
open={openFilePathInputModal}
setOpen={setOpenFilePathInputModal}
@@ -511,203 +228,3 @@ const 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
}

View 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

View 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

View File

@@ -0,0 +1,39 @@
import { RunTimeType } from '../../../context/appContext'
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 ?? ''
}
export const programPathInjection = (
code: string,
path: string,
runtime: RunTimeType
) => {
if (path) {
if (runtime === RunTimeType.JS) {
return `const _PROGRAM = '${path}';\n${code}`
}
if (runtime === RunTimeType.PY) {
return `_PROGRAM = '${path}';\n${code}`
}
if (runtime === RunTimeType.R) {
return `._PROGRAM = '${path}';\n${code}`
}
if (runtime === RunTimeType.SAS) {
return `%let _program = ${path};\n${code}`
}
}
return code
}

View File

@@ -0,0 +1,308 @@
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, programPathInjection } 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: programPathInjection(
code,
selectedFilePath,
selectedRunTime as RunTimeType
),
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()?.toLowerCase() ?? ''
)
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(() => {
const fileExtension = selectedFileExtension.toLowerCase()
if (runTimes.includes(fileExtension)) setSelectedRunTime(fileExtension)
}, [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

View File

@@ -16,7 +16,9 @@ export enum ModeType {
export enum RunTimeType {
SAS = 'sas',
JS = 'js'
JS = 'js',
PY = 'py',
R = 'r'
}
interface AppContextProps {

View 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

View File

@@ -1,2 +1,4 @@
export * from './useModal'
export * from './usePrompt'
export * from './useStateWithCallback'
export * from './useSnackbar'

View 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 }
}

View 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 }
}