Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92e0b8a088 | ||
|
|
b484306ed8 | ||
| 5e08aacc51 | |||
| a9e4eb685d | |||
| 31b09f27cc | |||
| 9f3ec92f8e | |||
| 6c9e449614 | |||
| 68e84b0994 | |||
| f0bb51a0d5 | |||
| b93a0da3a3 | |||
|
|
e5facbf54c | ||
|
|
cb2bebbe76 | ||
|
|
9e1e0ce8cc | ||
|
|
29928753b7 | ||
|
|
edd69ecaae | ||
|
|
74ba65f9f3 | ||
|
|
f257602834 | ||
|
|
61080d4694 | ||
|
|
82633adbc4 | ||
|
|
23db7e7b7d | ||
|
|
cbaa687c9b | ||
|
|
527f70e90d | ||
|
|
122faad55f | ||
|
|
3ff6f5e865 | ||
|
|
7d5128c0d6 | ||
|
|
e1ebbfd087 | ||
|
|
e430bdb0d4 | ||
|
|
9d9769eef3 | ||
|
|
9d167abe2a | ||
|
|
18d0604bdd | ||
|
|
7b7bc6b778 | ||
|
|
fb4f3442d5 | ||
|
|
09d1b7d5d4 | ||
|
|
99839ae62f | ||
|
|
f700561e1a | ||
|
|
8b4b4b91ab | ||
|
|
acb3ae0493 | ||
|
|
f48aeb1b0b | ||
|
|
5c0e8e5344 | ||
|
|
0ac9e4af7d | ||
|
|
ee80f3f968 | ||
|
|
7f4201ba85 | ||
|
|
f830bbc058 | ||
|
|
f8e1522a5a | ||
|
|
0a5aeceab5 | ||
|
|
6dc39c0d91 | ||
|
|
117a53ceea | ||
|
|
dd56a95314 | ||
|
|
c5117abe71 | ||
|
|
84c632a861 | ||
|
|
3ddd09eba0 | ||
|
|
0c0301433c | ||
|
|
954b2e3e2e | ||
|
|
5655311b96 | ||
|
|
9ace33d783 | ||
|
|
adc5aca0f0 | ||
|
|
71c6be6b84 | ||
|
|
9c751877d1 | ||
|
|
2204d54cd6 | ||
|
|
f4eb75ff34 | ||
|
|
a3cde343b7 | ||
|
|
7a70d40dbf | ||
|
|
d27e070fc8 | ||
|
|
27e260e6a4 | ||
|
|
2796db8ead | ||
|
|
84f7c2ab89 | ||
|
|
e68090181a | ||
|
|
d2956fc641 | ||
|
|
a701bb25e7 | ||
|
|
5758bcd392 | ||
|
|
9e53470947 | ||
|
|
81f6605249 | ||
|
|
0b45402946 | ||
|
|
9ac3191891 | ||
|
|
cd00aa2af8 | ||
|
|
0147bcb701 | ||
|
|
bf53ad30f4 | ||
|
|
a003b8836b | ||
|
|
df6003df94 | ||
|
|
b1d0fdbb02 | ||
|
|
2c34395110 | ||
|
|
534e4e5bf3 | ||
|
|
6146372eba | ||
|
|
aaa469a142 | ||
|
|
4fd5bf948e | ||
|
|
99f91fbce2 | ||
|
|
98a00ec7ac | ||
|
|
b0fb858c49 | ||
|
|
83959ef99e | ||
|
|
08087495d3 | ||
|
|
3f68474839 | ||
|
|
f26886f84d | ||
|
|
ddd50eac8e | ||
|
|
bba3e8d272 | ||
|
|
30944bfa18 | ||
|
|
8822de95df | ||
|
|
02a242fe4b | ||
|
|
1beac914db | ||
|
|
a45b42107e | ||
| 3d89b753f0 | |||
| fb77d99177 | |||
| fa627aabf9 | |||
|
|
fd2629862f | ||
|
|
75291f9397 | ||
|
|
99fb5f4b2b | ||
|
|
5dc3deeb11 | ||
|
|
6b708fcad3 | ||
|
|
bc0ff84d8d | ||
|
|
1ff6965dd2 | ||
|
|
d6fa877941 |
4
.github/CONTRIBUTING.md
vendored
@@ -113,3 +113,7 @@ cd ./api && npm i && npm run exe
|
||||
```
|
||||
|
||||
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
||||
|
||||
## Releases
|
||||
|
||||
To cut a release, run `npm run release` on the main branch, then push the tags (per the console log link)
|
||||
|
||||
188
CHANGELOG.md
@@ -2,6 +2,194 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.0.51](https://github.com/sasjs/server/compare/v0.0.50...v0.0.51) (2022-04-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* run button running man, sub menu added ([68e84b0](https://github.com/sasjs/server/commit/68e84b0994a3fa6ff56b07635c637c6e3a57bfda))
|
||||
* running code with CTRL+ENTER ([b93a0da](https://github.com/sasjs/server/commit/b93a0da3a380926c87548b69309b2d0c1b7e617f))
|
||||
|
||||
### [0.0.50](https://github.com/sasjs/server/compare/v0.0.49...v0.0.50) (2022-04-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **appstream:** Upload an app from appStream page ([74ba65f](https://github.com/sasjs/server/commit/74ba65f9f330bf8c98c12a9c66bb60773d5a7b77))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* session death time has to be a valid string number ([23db7e7](https://github.com/sasjs/server/commit/23db7e7b7df2f22bbf7ce16865f83091624d8047))
|
||||
* web component added tooltip for webout in studio ([61080d4](https://github.com/sasjs/server/commit/61080d4694859306049346d2e3174f27bb6dac16))
|
||||
* web component UI fix for studio scrolling ([f257602](https://github.com/sasjs/server/commit/f25760283492140cc1f14e51ed27673ec28baaf3))
|
||||
|
||||
### [0.0.49](https://github.com/sasjs/server/compare/v0.0.48...v0.0.49) (2022-04-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **stp:** read file in non-binary mode if debug one ([527f70e](https://github.com/sasjs/server/commit/527f70e90dd7369766e375ac2d6fc38b2a114d11))
|
||||
|
||||
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **deploy:** new route added for deploy with build.json ([18d0604](https://github.com/sasjs/server/commit/18d0604bdd0b20ad468f9345474b4de034ee3a67))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove uploaded build.json from temp folder in all cases ([9d167ab](https://github.com/sasjs/server/commit/9d167abe2adb743bca161862b4561bf573182c00))
|
||||
* **stp:** return log+webout for debug on ([3ff6f5e](https://github.com/sasjs/server/commit/3ff6f5e86581cd2ac23bbe0b8e2c367fbea890ed))
|
||||
|
||||
### [0.0.47](https://github.com/sasjs/server/compare/v0.0.46...v0.0.47) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **web:** updated STUDIO log and webout ([f700561](https://github.com/sasjs/server/commit/f700561e1a8d06c18ca2bdbe4605d7ab34f7a761))
|
||||
|
||||
### [0.0.46](https://github.com/sasjs/server/compare/v0.0.45...v0.0.46) (2022-03-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **drive:** GET folder contents API added ([0ac9e4a](https://github.com/sasjs/server/commit/0ac9e4af7d67c4431053e80eb2384bf5bdc3f8b3))
|
||||
|
||||
### [0.0.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
|
||||
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||
|
||||
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||
|
||||
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
|
||||
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
|
||||
|
||||
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
|
||||
|
||||
### [0.0.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
|
||||
|
||||
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
|
||||
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
|
||||
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
|
||||
|
||||
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* included sasjs core macros at compile time ([e680901](https://github.com/sasjs/server/commit/e68090181acd844f86f3e81153cb5a4e3f4a307f))
|
||||
|
||||
### [0.0.38](https://github.com/sasjs/server/compare/v0.0.37...v0.0.38) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* quick fix for executables ([9e53470](https://github.com/sasjs/server/commit/9e53470947350f4b8d835a2cb6b70e3dabf247c4))
|
||||
|
||||
### [0.0.37](https://github.com/sasjs/server/compare/v0.0.36...v0.0.37) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* appStream html view ([cd00aa2](https://github.com/sasjs/server/commit/cd00aa2af8c7e0df851050a02152dfeddaec7b0f))
|
||||
* moved macros from codebase to drive ([9ac3191](https://github.com/sasjs/server/commit/9ac3191891bf53ff07135ccec6ddc83b34ea871a))
|
||||
* **webin:** closes [#99](https://github.com/sasjs/server/issues/99) ([0147bcb](https://github.com/sasjs/server/commit/0147bcb701a209266144147a3746baf1eb1ccc63))
|
||||
|
||||
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* App Stream, load on startup, new route added ([98a00ec](https://github.com/sasjs/server/commit/98a00ec7ace5da765f049864799be44ba6538e8a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **appstream:** app logo + improvements ([df6003d](https://github.com/sasjs/server/commit/df6003df942fd52b956f3d4069d6d7615441d372))
|
||||
|
||||
### [0.0.35](https://github.com/sasjs/server/compare/v0.0.33...v0.0.35) (2022-03-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cors:** whitelisting is configurable through .env variables ([99f91fb](https://github.com/sasjs/server/commit/99f91fbce2a029dd963ed30c9007a9b046ea6560))
|
||||
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cors:** removed trailing slashes of urls ([4fd5bf9](https://github.com/sasjs/server/commit/4fd5bf948e4ad8a274d3176d5509163e67980061))
|
||||
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
|
||||
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
|
||||
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
|
||||
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
|
||||
|
||||
### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
|
||||
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
|
||||
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
|
||||
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
|
||||
|
||||
### [0.0.33](https://github.com/sasjs/server/compare/v0.0.32...v0.0.33) (2022-03-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* serve deployed streaming apps ([d6fa877](https://github.com/sasjs/server/commit/d6fa87794155880adc23c2552c37c86ad606c292))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adde validation + code improvement ([1ff6965](https://github.com/sasjs/server/commit/1ff6965dd2f44ad74136af04b4fba8c76979ecba))
|
||||
* added api button on web component ([6b708fc](https://github.com/sasjs/server/commit/6b708fcad30d92c21713f9c97bca173c148cc875))
|
||||
|
||||
### [0.0.32](https://github.com/sasjs/server/compare/v0.0.31...v0.0.32) (2022-03-14)
|
||||
|
||||
|
||||
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 SASjs
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
130
README.md
@@ -1,54 +1,21 @@
|
||||
# SASjs Server
|
||||
|
||||
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or it could even run locally on your desktop. It provides the following functionality:
|
||||
SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or locally on your desktop. It provides:
|
||||
|
||||
- Virtual filesystem for storing SAS programs and other content
|
||||
- Ability to execute Stored Programs from a URL
|
||||
- Ability to create web apps using simple Desktop SAS
|
||||
- REST API with Swagger Docs
|
||||
|
||||
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
||||
One major benefit of using SASjs Server alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library, is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
|
||||
|
||||
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentiation, and a database)
|
||||
SASjs Server is available in two modes - Desktop (without authentication) and Server (with authentication, and a database)
|
||||
|
||||
## Installation
|
||||
|
||||
## Configuration
|
||||
Installation can be made programmatically using command line, or by manually downloading and running the executable.
|
||||
|
||||
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||
|
||||
- Configured globally in /etc/environment file
|
||||
- Export in terminal or shell script (`export VAR=VALUE`)
|
||||
- Prepend in command
|
||||
- Enter in the `.env` file alongside the executable
|
||||
|
||||
Example variables:
|
||||
|
||||
```
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
PORT=[5000] default value is 5000
|
||||
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
||||
SAS_PATH=/path/to/sas/executable.exe
|
||||
DRIVE_PATH=./tmp
|
||||
PROTOCOL=[http|https] default considered as http. Use pems below if htttps.
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
```
|
||||
|
||||
## Desktop Version
|
||||
|
||||
### Manual Installation
|
||||
|
||||
Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||
|
||||
Next, trigger by double clicking (windows) or executing from commandline.
|
||||
|
||||
You are presented with two prompts (if not set as ENV vars):
|
||||
|
||||
- Location of your `sas.exe` / `sas.sh` executable
|
||||
- Path to a filesystem location for Stored Programs and temporary files
|
||||
|
||||
## Programmatic Installation
|
||||
### Programmatic
|
||||
|
||||
Fetch the relevant package from github using `curl`, eg as follows (for linux):
|
||||
|
||||
@@ -59,6 +26,78 @@ unzip linux.zip
|
||||
|
||||
The app can then be launched with `./api-linux` and prompts followed (if ENV vars not set).
|
||||
|
||||
### Manual
|
||||
|
||||
1. Download the relevant package from the [releases](https://github.com/sasjs/server/releases) page
|
||||
2. Trigger by double clicking (windows) or executing from commandline.
|
||||
|
||||
You are presented with two prompts (if not set as ENV vars):
|
||||
|
||||
- Location of your `sas.exe` / `sas.sh` executable
|
||||
- Path to a filesystem location for Stored Programs and temporary files
|
||||
|
||||
## ENV Var configuration
|
||||
|
||||
When launching the app, it will make use of specific environment variables. These can be set in the following places:
|
||||
|
||||
- Configured globally in `/etc/environment` file
|
||||
- Export in terminal or shell script (`export VAR=VALUE`)
|
||||
- Prepended in the command
|
||||
- Enter in the `.env` file alongside the executable
|
||||
|
||||
Example contents of a `.env` file:
|
||||
|
||||
```
|
||||
# options: [desktop|server] default: `desktop`
|
||||
MODE=
|
||||
|
||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||
CORS=
|
||||
|
||||
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||
WHITELIST=
|
||||
|
||||
# options: [http|https] default: http
|
||||
PROTOCOL=
|
||||
|
||||
# default: 5000
|
||||
PORT=
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
|
||||
SAS_PATH=/path/to/sas/executable.exe
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` defaults to /tmp
|
||||
DRIVE_PATH=/tmp
|
||||
|
||||
# ENV variables required for PROTOCOL: `https`
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
|
||||
# ENV variables required for MODE: `server`
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||
|
||||
# SAS Options
|
||||
# On windows use SAS_OPTIONS and on unix use SASV9_OPTIONS
|
||||
# Any options set here are automatically applied in the SAS session
|
||||
# See: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostunx/p0wrdmqp8k0oyyn1xbx3bp3qy2wl.htm
|
||||
# And: https://documentation.sas.com/doc/en/pgmsascdc/9.4_3.5/hostwin/p0drw76qo0gig2n1kcoliekh605k.htm#p09y7hx0grw1gin1giuvrjyx61m6
|
||||
SAS_OPTIONS= -NOXCMD
|
||||
SASV9_OPTIONS= -NOXCMD
|
||||
|
||||
```
|
||||
|
||||
## Persisting the Session
|
||||
|
||||
Normally the server process will stop when your terminal dies. To keep it going you can use the npm package [pm2](https://www.npmjs.com/package/pm2) (`npm install pm2@latest -g`) as follows:
|
||||
|
||||
```bash
|
||||
@@ -69,7 +108,7 @@ export DRIVE_PATH=./tmp
|
||||
pm2 start api-linux
|
||||
```
|
||||
|
||||
To get the logs (and some usefull commands):
|
||||
To get the logs (and some useful commands):
|
||||
|
||||
```bash
|
||||
pm2 [list|ls|status]
|
||||
@@ -91,11 +130,10 @@ Instead of `app_name` you can pass:
|
||||
- `all` to act on all processes
|
||||
- `id` to act on a specific process id
|
||||
|
||||
|
||||
## Server Version
|
||||
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
|
||||
* CLIENTID: `clientID1`
|
||||
* USERNAME: `secretuser`
|
||||
* PASSWORD: `secretpassword`
|
||||
- CLIENTID: `clientID1`
|
||||
- USERNAME: `secretuser`
|
||||
- PASSWORD: `secretpassword`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
MODE=[desktop|server] default considered as desktop
|
||||
CORS=[disable|enable] default considered as disable
|
||||
CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
|
||||
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
|
||||
PROTOCOL=[http|https] default considered as http
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
PORT=[5000] default value is 5000
|
||||
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
|
||||
1
api/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v16.14.0
|
||||
420
api/package-lock.json
generated
@@ -9,14 +9,14 @@
|
||||
"prebuild": "npm run initial",
|
||||
"start": "nodemon ./src/server.ts",
|
||||
"build": "rimraf build && tsc",
|
||||
"postbuild": "npm run copy:files",
|
||||
"swagger": "tsoa spec",
|
||||
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true",
|
||||
"test": "mkdir -p tmp && mkdir -p ../web/build && jest --silent --coverage",
|
||||
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"lint": "npx prettier --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"",
|
||||
"package:lib": "npm run build && cp ./package.json build && cp README.md build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"exe": "npm run build && npm run exe:copy && pkg .",
|
||||
"exe:copy": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||
"exe": "npm run build && pkg .",
|
||||
"copy:files": "npm run public:copy && npm run sasjsbuild:copy && npm run sasjscore:copy && npm run web:copy",
|
||||
"public:copy": "cp -r ./public/ ./build/public/",
|
||||
"sasjsbuild:copy": "cp -r ./sasjsbuild/ ./build/sasjsbuild/",
|
||||
"sasjscore:copy": "cp -r ./sasjscore/ ./build/sasjscore/",
|
||||
@@ -29,6 +29,7 @@
|
||||
"assets": [
|
||||
"./build/public/**/*",
|
||||
"./build/sasjsbuild/**/*",
|
||||
"./build/sasjscore/**/*",
|
||||
"./web/build/**/*"
|
||||
],
|
||||
"targets": [
|
||||
@@ -46,7 +47,7 @@
|
||||
"author": "4GL Ltd",
|
||||
"dependencies": {
|
||||
"@sasjs/core": "4.9.0",
|
||||
"@sasjs/utils": "2.34.1",
|
||||
"@sasjs/utils": "2.42.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
@@ -57,8 +58,7 @@
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.3",
|
||||
"swagger-ui-express": "^4.1.6",
|
||||
"tsoa": "3.14.1"
|
||||
"swagger-ui-express": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
@@ -84,9 +84,15 @@
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsoa": "3.14.1",
|
||||
"typescript": "^4.3.2"
|
||||
},
|
||||
"configuration": {
|
||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"tmp/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
api/public/plus.png
Normal file
|
After Width: | Height: | Size: 899 B |
21
api/public/sasjs-logo.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F6E40C;}
|
||||
</style>
|
||||
<rect id="XMLID_1_" width="32" height="32"/>
|
||||
<g id="XMLID_654_">
|
||||
<path id="XMLID_656_" class="st0" d="M27.9,17.4c-1.1,0-2.1,0-3,0c-1.2,0-2.3,0-3.5,0c-0.5,0-0.7,0.2-0.6,0.7c0,2.1,0,4.3,0,6.4
|
||||
c0,0.5-0.2,0.8-0.6,1c-2.5,1.4-4.9,2.8-7.3,4.3c-0.4,0.2-0.6,0.2-1,0c-2.4-1.4-4.9-2.9-7.3-4.3c-0.2-0.1-0.5-0.5-0.5-0.7
|
||||
c0-3.2,0-6.4,0-9.6c0-0.1,0-0.1,0.1-0.3c0.3,0,0.5,0,0.8,0c1.9,0,3.7,0,5.6,0c0.6,0,0.7-0.2,0.7-0.7c0-2.1,0-4.2,0-6.4
|
||||
c0-0.5,0.1-0.8,0.6-1.1c2.5-1.4,4.9-2.9,7.3-4.3c0.2-0.1,0.6-0.1,0.9,0c2.5,1.4,5,2.9,7.5,4.4c0.2,0.1,0.4,0.4,0.4,0.6
|
||||
C27.9,10.6,27.9,13.9,27.9,17.4z M20.8,14.8c1.4,0,2.7,0,4,0c0.5,0,0.7-0.2,0.7-0.7c0-1.7,0-3.3,0-5c0-0.5-0.2-0.7-0.6-1
|
||||
c-1.6-0.9-3.2-1.9-4.8-2.8c-0.2-0.1-0.7-0.1-0.9,0c-1.6,0.9-3.2,1.9-4.8,2.8c-0.4,0.2-0.6,0.5-0.6,1c0,3.2,0,6.3,0,9.5
|
||||
c0,1.9,0,1.9-1.9,1.9c-0.4,0-0.6-0.1-0.6-0.6c0-0.6,0-1.3,0-1.9c0-0.5-0.2-0.6-0.6-0.6c-1.1,0-2.2,0-3.3,0c-0.5,0-0.7,0.2-0.7,0.7
|
||||
c0,1.6,0,3.3,0,4.9c0,0.5,0.2,0.8,0.6,1c1.6,0.9,3.2,1.9,4.8,2.8c0.2,0.1,0.7,0.1,0.9,0c1.6-0.9,3.2-1.9,4.8-2.8
|
||||
c0.4-0.2,0.6-0.5,0.6-1c0-3.1,0-6.1,0-9.2c0-1.9,0-1.9,1.9-1.9c0.5,0,0.7,0.2,0.7,0.7C20.8,13.3,20.8,14,20.8,14.8z"/>
|
||||
<path id="XMLID_655_" class="st0" d="M18,2.1l-6.8,3.9V2.7c0-0.3,0.3-0.6,0.6-0.6H18z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -161,6 +161,8 @@ components:
|
||||
$ref: '#/components/schemas/FolderMember'
|
||||
-
|
||||
$ref: '#/components/schemas/ServiceMember'
|
||||
-
|
||||
$ref: '#/components/schemas/FileMember'
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
@@ -186,6 +188,24 @@ components:
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
MemberType.file:
|
||||
enum:
|
||||
- file
|
||||
type: string
|
||||
FileMember:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/MemberType.file'
|
||||
code:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
- code
|
||||
type: object
|
||||
additionalProperties: false
|
||||
FileTree:
|
||||
properties:
|
||||
members:
|
||||
@@ -195,6 +215,8 @@ components:
|
||||
$ref: '#/components/schemas/FolderMember'
|
||||
-
|
||||
$ref: '#/components/schemas/ServiceMember'
|
||||
-
|
||||
$ref: '#/components/schemas/FileMember'
|
||||
type: array
|
||||
required:
|
||||
- members
|
||||
@@ -206,6 +228,8 @@ components:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
streamServiceName:
|
||||
type: string
|
||||
example:
|
||||
$ref: '#/components/schemas/FileTree'
|
||||
required:
|
||||
@@ -217,9 +241,12 @@ components:
|
||||
properties:
|
||||
appLoc:
|
||||
type: string
|
||||
streamWebFolder:
|
||||
type: string
|
||||
fileTree:
|
||||
$ref: '#/components/schemas/FileTree'
|
||||
required:
|
||||
- appLoc
|
||||
- fileTree
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -579,13 +606,62 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployPayload'
|
||||
/SASjsApi/drive/deploy/upload:
|
||||
post:
|
||||
operationId: DeployUpload
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: success, message: 'Files deployed successfully to @sasjs/server.'}
|
||||
'400':
|
||||
description: 'Invalid Format'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Provided not supported data format.'}
|
||||
'500':
|
||||
description: 'Execution Error'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DeployResponse'
|
||||
examples:
|
||||
'Example 1':
|
||||
value: {status: failure, message: 'Deployment failed!'}
|
||||
summary: 'Creates/updates files within SASjs Drive using uploaded JSON file.'
|
||||
tags:
|
||||
- Drive
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
required:
|
||||
- file
|
||||
/SASjsApi/drive/file:
|
||||
get:
|
||||
operationId: GetFile
|
||||
responses:
|
||||
'204':
|
||||
description: 'No content'
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||
summary: 'Get file from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -596,19 +672,10 @@ paths:
|
||||
-
|
||||
in: query
|
||||
name: _filePath
|
||||
required: false
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
filePath:
|
||||
type: string
|
||||
delete:
|
||||
operationId: DeleteFile
|
||||
responses:
|
||||
@@ -622,7 +689,6 @@ paths:
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||
summary: 'Delete file from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -633,19 +699,10 @@ paths:
|
||||
-
|
||||
in: query
|
||||
name: _filePath
|
||||
required: false
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
filePath:
|
||||
type: string
|
||||
post:
|
||||
operationId: SaveFile
|
||||
responses:
|
||||
@@ -748,6 +805,36 @@ paths:
|
||||
type: string
|
||||
required:
|
||||
- file
|
||||
/SASjsApi/drive/folder:
|
||||
get:
|
||||
operationId: GetFolder
|
||||
responses:
|
||||
'200':
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
folders: {items: {type: string}, type: array}
|
||||
files: {items: {type: string}, type: array}
|
||||
required:
|
||||
- folders
|
||||
- files
|
||||
type: object
|
||||
summary: 'Get folder contents from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
in: query
|
||||
name: _folderPath
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder
|
||||
/SASjsApi/drive/filetree:
|
||||
get:
|
||||
operationId: GetFileTree
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
CompileTree,
|
||||
createFile,
|
||||
loadDependenciesFile,
|
||||
readFile,
|
||||
@@ -17,7 +18,9 @@ const compiledSystemInit = async (systemInit: string) =>
|
||||
programFolders: [],
|
||||
macroFolders: [],
|
||||
buildSourceFolder: '',
|
||||
macroCorePath
|
||||
binaryFolders: [],
|
||||
macroCorePath,
|
||||
compileTree: new CompileTree('') // dummy compileTree
|
||||
}))
|
||||
|
||||
const createSysInitFile = async () => {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import path from 'path'
|
||||
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils'
|
||||
import {
|
||||
asyncForEach,
|
||||
copy,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
listFilesInFolder
|
||||
} from '@sasjs/utils'
|
||||
|
||||
import { apiRoot, sasJSCoreMacros } from '../src/utils'
|
||||
import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
|
||||
|
||||
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
|
||||
|
||||
@@ -16,6 +23,10 @@ export const copySASjsCore = async () => {
|
||||
|
||||
await copy(coreSubFolderPath, sasJSCoreMacros)
|
||||
})
|
||||
|
||||
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||
|
||||
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||
}
|
||||
|
||||
copySASjsCore()
|
||||
|
||||
@@ -7,31 +7,35 @@ import cors from 'cors'
|
||||
|
||||
import {
|
||||
connectDB,
|
||||
copySASjsCore,
|
||||
getWebBuildFolderPath,
|
||||
sasJSCoreMacros,
|
||||
setProcessVariables
|
||||
loadAppStreamConfig,
|
||||
setProcessVariables,
|
||||
setupFolders
|
||||
} from './utils'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const app = express()
|
||||
|
||||
const { MODE, CORS, PORT_WEB } = process.env
|
||||
const whiteList = [
|
||||
`http://localhost:${PORT_WEB ?? 3000}`,
|
||||
'https://sas.analytium.co.uk:8343'
|
||||
]
|
||||
const { MODE, CORS, WHITELIST } = process.env
|
||||
|
||||
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
|
||||
console.log('All CORS Requests are enabled')
|
||||
const whiteList: string[] = []
|
||||
WHITELIST?.split(' ')?.forEach((url) => {
|
||||
if (url.startsWith('http'))
|
||||
// removing trailing slash of URLs listing for CORS
|
||||
whiteList.push(url.replace(/\/$/, ''))
|
||||
})
|
||||
|
||||
console.log('All CORS Requests are enabled for:', whiteList)
|
||||
app.use(cors({ credentials: true, origin: whiteList }))
|
||||
}
|
||||
|
||||
app.use(cookieParser())
|
||||
app.use(morgan('tiny'))
|
||||
app.use(express.json({ limit: '50mb' }))
|
||||
app.use(express.json({ limit: '100mb' }))
|
||||
app.use(express.static(path.join(__dirname, '../public')))
|
||||
app.use(express.static(getWebBuildFolderPath()))
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
console.error(err.stack)
|
||||
@@ -39,12 +43,19 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
}
|
||||
|
||||
export default setProcessVariables().then(async () => {
|
||||
await setupFolders()
|
||||
await copySASjsCore()
|
||||
|
||||
// loading these modules after setting up variables due to
|
||||
// multer's usage of process var process.driveLoc
|
||||
const { setupRoutes } = await import('./routes/setupRoutes')
|
||||
setupRoutes(app)
|
||||
|
||||
console.log('sasJSCoreMacros', sasJSCoreMacros)
|
||||
await loadAppStreamConfig()
|
||||
|
||||
// should be served after setting up web route
|
||||
// index.html needs to be injected with some js script.
|
||||
app.use(express.static(getWebBuildFolderPath()))
|
||||
|
||||
app.use(onError)
|
||||
|
||||
|
||||
@@ -14,27 +14,36 @@ import {
|
||||
Patch,
|
||||
UploadedFile,
|
||||
FormField,
|
||||
Delete
|
||||
Delete,
|
||||
Hidden
|
||||
} from 'tsoa'
|
||||
import {
|
||||
fileExists,
|
||||
moveFile,
|
||||
createFolder,
|
||||
deleteFile as deleteFileOnSystem
|
||||
deleteFile as deleteFileOnSystem,
|
||||
folderExists,
|
||||
listFilesInFolder,
|
||||
listSubFoldersInFolder,
|
||||
isFolder,
|
||||
FileTree,
|
||||
isFileTree
|
||||
} from '@sasjs/utils'
|
||||
import { createFileTree, ExecutionController, getTreeExample } from './internal'
|
||||
|
||||
import { FileTree, isFileTree, TreeNode } from '../types'
|
||||
import { TreeNode } from '../types'
|
||||
import { getTmpFilesFolderPath } from '../utils'
|
||||
|
||||
interface DeployPayload {
|
||||
appLoc?: string
|
||||
appLoc: string
|
||||
streamWebFolder?: string
|
||||
fileTree: FileTree
|
||||
}
|
||||
|
||||
interface DeployResponse {
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: FileTree
|
||||
}
|
||||
|
||||
@@ -87,9 +96,21 @@ export class DriveController {
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
* @summary Creates/updates files within SASjs Drive using uploaded JSON file.
|
||||
*
|
||||
*/
|
||||
@Example<DeployResponse>(successDeployResponse)
|
||||
@Response<DeployResponse>(400, 'Invalid Format', invalidDeployFormatResponse)
|
||||
@Response<DeployResponse>(500, 'Execution Error', execDeployErrorResponse)
|
||||
@Post('/deploy/upload')
|
||||
public async deployUpload(
|
||||
@UploadedFile() file: Express.Multer.File, // passing here for API docs
|
||||
@Query() @Hidden() body?: DeployPayload // Hidden decorator has be optional
|
||||
): Promise<DeployResponse> {
|
||||
return deploy(body!)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
@@ -98,28 +119,31 @@ export class DriveController {
|
||||
@Get('/file')
|
||||
public async getFile(
|
||||
@Request() request: express.Request,
|
||||
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
@Query() _filePath: string
|
||||
) {
|
||||
return getFile(request, (_filePath ?? filePath)!)
|
||||
return getFile(request, _filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @summary Get folder contents from SASjs Drive
|
||||
* @query _folderPath Location of SAS program
|
||||
* @example _folderPath "/Public/somefolder"
|
||||
*/
|
||||
@Get('/folder')
|
||||
public async getFolder(@Query() _folderPath?: string) {
|
||||
return getFolder(_folderPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Delete file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Delete('/file')
|
||||
public async deleteFile(
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
) {
|
||||
return deleteFile((_filePath ?? filePath)!)
|
||||
public async deleteFile(@Query() _filePath: string) {
|
||||
return deleteFile(_filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,14 +214,23 @@ const getFileTree = () => {
|
||||
}
|
||||
|
||||
const deploy = async (data: DeployPayload) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
|
||||
|
||||
const appLocPath = path
|
||||
.join(getTmpFilesFolderPath(), ...appLocParts)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!appLocPath.includes(driveFilesPath)) {
|
||||
throw new Error('appLoc cannot be outside drive.')
|
||||
}
|
||||
|
||||
if (!isFileTree(data.fileTree)) {
|
||||
throw { code: 400, ...invalidDeployFormatResponse }
|
||||
}
|
||||
|
||||
await createFileTree(
|
||||
data.fileTree.members,
|
||||
data.appLoc ? data.appLoc.replace(/^\//, '').split('/') : []
|
||||
).catch((err) => {
|
||||
await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
|
||||
throw { code: 500, ...execDeployErrorResponse, ...err }
|
||||
})
|
||||
|
||||
@@ -216,7 +249,7 @@ const getFile = async (req: express.Request, filePath: string) => {
|
||||
}
|
||||
|
||||
if (!(await fileExists(filePathFull))) {
|
||||
throw new Error('File does not exist.')
|
||||
throw new Error("File doesn't exist.")
|
||||
}
|
||||
|
||||
const extension = path.extname(filePathFull).toLowerCase()
|
||||
@@ -227,6 +260,36 @@ const getFile = async (req: express.Request, filePath: string) => {
|
||||
req.res?.sendFile(path.resolve(filePathFull))
|
||||
}
|
||||
|
||||
const getFolder = async (folderPath?: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
if (folderPath) {
|
||||
const folderPathFull = path
|
||||
.join(getTmpFilesFolderPath(), folderPath)
|
||||
.replace(new RegExp('/', 'g'), path.sep)
|
||||
|
||||
if (!folderPathFull.includes(driveFilesPath)) {
|
||||
throw new Error('Cannot get folder outside drive.')
|
||||
}
|
||||
|
||||
if (!(await folderExists(folderPathFull))) {
|
||||
throw new Error("Folder doesn't exist.")
|
||||
}
|
||||
|
||||
if (!(await isFolder(folderPathFull))) {
|
||||
throw new Error('Not a Folder.')
|
||||
}
|
||||
|
||||
const files: string[] = await listFilesInFolder(folderPathFull)
|
||||
const folders: string[] = await listSubFoldersInFolder(folderPathFull)
|
||||
return { files, folders }
|
||||
}
|
||||
|
||||
const files: string[] = await listFilesInFolder(driveFilesPath)
|
||||
const folders: string[] = await listSubFoldersInFolder(driveFilesPath)
|
||||
return { files, folders }
|
||||
}
|
||||
|
||||
const deleteFile = async (filePath: string) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
@@ -294,9 +357,3 @@ const updateFile = async (
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const validateFilePath = async (filePath: string) => {
|
||||
if (!(await fileExists(filePath))) {
|
||||
throw 'DriveController: File does not exists.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
moveFile,
|
||||
readFileBinary
|
||||
} from '@sasjs/utils'
|
||||
import { PreProgramVars, TreeNode } from '../../types'
|
||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||
import {
|
||||
extractHeaders,
|
||||
generateFileUploadSasCode,
|
||||
getTmpFilesFolderPath,
|
||||
getTmpMacrosPath,
|
||||
HTTPHeaders,
|
||||
isDebugOn,
|
||||
sasJSCoreMacros
|
||||
isDebugOn
|
||||
} from '../../utils'
|
||||
|
||||
export interface ExecutionVars {
|
||||
@@ -39,7 +39,8 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
session?: Session
|
||||
) {
|
||||
if (!(await fileExists(programPath)))
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
@@ -51,7 +52,8 @@ export class ExecutionController {
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson
|
||||
returnJson,
|
||||
session
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,11 +62,13 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
sessionByFileUpload?: Session
|
||||
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController = getSessionController()
|
||||
|
||||
const session = await sessionController.getSession()
|
||||
const session =
|
||||
sessionByFileUpload ?? (await sessionController.getSession())
|
||||
session.inUse = true
|
||||
session.consumed = true
|
||||
|
||||
@@ -106,7 +110,7 @@ export class ExecutionController {
|
||||
`
|
||||
|
||||
program = `
|
||||
options insert=(SASAUTOS="${sasJSCoreMacros}");
|
||||
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
||||
|
||||
/* runtime vars */
|
||||
${varStatments}
|
||||
@@ -119,7 +123,7 @@ ${preProgramVarStatments}
|
||||
${program}`
|
||||
|
||||
// if no files are uploaded filesNamesMap will be undefined
|
||||
if (otherArgs && otherArgs.filesNamesMap) {
|
||||
if (otherArgs?.filesNamesMap) {
|
||||
const uploadSasCode = await generateFileUploadSasCode(
|
||||
otherArgs.filesNamesMap,
|
||||
session.path
|
||||
@@ -153,7 +157,9 @@ ${program}`
|
||||
: ''
|
||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||
const fileResponse: boolean =
|
||||
httpHeaders.hasOwnProperty('content-type') && !returnJson
|
||||
httpHeaders.hasOwnProperty('content-type') &&
|
||||
!returnJson && // not a POST Request
|
||||
!isDebugOn(vars) // Debug is not enabled
|
||||
|
||||
const webout = (await fileExists(weboutPath))
|
||||
? fileResponse
|
||||
@@ -174,11 +180,10 @@ ${program}`
|
||||
|
||||
return {
|
||||
httpHeaders,
|
||||
result: fileResponse
|
||||
? webout
|
||||
: isDebugOn(vars) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
result:
|
||||
isDebugOn(vars) || session.crashed
|
||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
||||
: webout
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import multer from 'multer'
|
||||
import { uuidv4 } from '@sasjs/utils'
|
||||
import { getSessionController } from '.'
|
||||
const multer = require('multer')
|
||||
|
||||
export class FileUploadController {
|
||||
private storage = multer.diskStorage({
|
||||
@@ -23,7 +23,9 @@ export class FileUploadController {
|
||||
|
||||
const sessionController = getSessionController()
|
||||
session = await sessionController.getSession()
|
||||
session.inUse = true
|
||||
// marking consumed true, so that it's not available
|
||||
// as readySession for any other request
|
||||
session.consumed = true
|
||||
|
||||
req.sasSession = session
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
createFile,
|
||||
fileExists,
|
||||
generateTimestamp,
|
||||
readFile,
|
||||
moveFile
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
const execFilePromise = promisify(execFile)
|
||||
@@ -31,7 +30,7 @@ export class SessionController {
|
||||
? readySessions[0]
|
||||
: await this.createSession()
|
||||
|
||||
if (readySessions.length < 2) this.createSession()
|
||||
if (readySessions.length < 3) this.createSession()
|
||||
|
||||
return session
|
||||
}
|
||||
@@ -41,6 +40,7 @@ export class SessionController {
|
||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||
|
||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||
// death time of session is 15 mins from creation
|
||||
const deathTimeStamp = (
|
||||
parseInt(creationTimeStamp) +
|
||||
15 * 60 * 1000 -
|
||||
@@ -87,6 +87,8 @@ ${autoExecContent}`
|
||||
codePath,
|
||||
'-LOG',
|
||||
path.join(session.path, 'log.log'),
|
||||
'-PRINT',
|
||||
path.join(session.path, 'output.lst'),
|
||||
'-WORK',
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
@@ -138,7 +140,9 @@ ${autoExecContent}`
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
session.deathTimeStamp = session.deathTimeStamp + 1000 * 10
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
|
||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||
import path from 'path'
|
||||
import { getTmpFilesFolderPath } from '../../utils/file'
|
||||
import {
|
||||
createFolder,
|
||||
createFile,
|
||||
asyncForEach,
|
||||
FolderMember,
|
||||
ServiceMember,
|
||||
FileMember,
|
||||
MemberType,
|
||||
FileTree
|
||||
} from '@sasjs/utils'
|
||||
|
||||
// REFACTOR: export FileTreeCpntroller
|
||||
export const createFileTree = async (
|
||||
members: (FolderMember | ServiceMember)[],
|
||||
members: (FolderMember | ServiceMember | FileMember)[],
|
||||
parentFolders: string[] = []
|
||||
) => {
|
||||
const destinationPath = path.join(
|
||||
@@ -13,25 +21,32 @@ export const createFileTree = async (
|
||||
path.join(...parentFolders)
|
||||
)
|
||||
|
||||
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
|
||||
let name = member.name
|
||||
await asyncForEach(
|
||||
members,
|
||||
async (member: FolderMember | ServiceMember | FileMember) => {
|
||||
let name = member.name
|
||||
|
||||
if (member.type === MemberType.service) name += '.sas'
|
||||
if (member.type === MemberType.service) name += '.sas'
|
||||
|
||||
if (member.type === MemberType.folder) {
|
||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||
Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
if (member.type === MemberType.folder) {
|
||||
await createFolder(path.join(destinationPath, name)).catch((err) =>
|
||||
Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
|
||||
await createFileTree(member.members, [...parentFolders, name]).catch(
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
} else {
|
||||
await createFile(path.join(destinationPath, name), member.code).catch(
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
await createFileTree(member.members, [...parentFolders, name]).catch(
|
||||
(err) => Promise.reject({ error: err, failedToCreate: name })
|
||||
)
|
||||
} else {
|
||||
const encoding = member.type === MemberType.file ? 'base64' : undefined
|
||||
|
||||
await createFile(
|
||||
path.join(destinationPath, name),
|
||||
member.code,
|
||||
encoding
|
||||
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -143,9 +143,8 @@ const executeReturnRaw = async (
|
||||
query
|
||||
)) as ExecuteReturnRaw
|
||||
|
||||
// Should over-ride response header for
|
||||
// debug on GET request to see entire log
|
||||
// rendering on browser.
|
||||
// Should over-ride response header for debug
|
||||
// on GET request to see entire log rendering on browser.
|
||||
if (isDebugOn(query)) {
|
||||
httpHeaders['content-type'] = 'text/plain'
|
||||
}
|
||||
@@ -185,7 +184,8 @@ const executeReturnJson = async (
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true
|
||||
true,
|
||||
req.sasSession
|
||||
)) as ExecuteReturnJson
|
||||
|
||||
let weboutRes: string | IRecordOfAny = webout
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import path from 'path'
|
||||
import { Request } from 'express'
|
||||
import multer, { FileFilterCallback, Options } from 'multer'
|
||||
import { getTmpUploadsPath } from '../utils'
|
||||
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
||||
|
||||
const acceptableExtensions = ['.sas']
|
||||
const fieldNameSize = 300
|
||||
const fileSize = 10485760 // 10 MB
|
||||
const fileSize = 104857600 // 100 MB
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: getTmpUploadsPath(),
|
||||
@@ -31,15 +30,11 @@ const fileFilter: Options['fileFilter'] = (
|
||||
file: Express.Multer.File,
|
||||
callback: FileFilterCallback
|
||||
) => {
|
||||
const fileExtension = path.extname(file.originalname).toLocaleLowerCase()
|
||||
|
||||
if (!acceptableExtensions.includes(fileExtension)) {
|
||||
const fileExtension = path.extname(file.originalname)
|
||||
const shouldBlockUpload = blockFileRegex.test(file.originalname)
|
||||
if (shouldBlockUpload) {
|
||||
return callback(
|
||||
new Error(
|
||||
`File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
new Error(`File extension '${fileExtension}' not acceptable.`)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import express from 'express'
|
||||
import { deleteFile } from '@sasjs/utils'
|
||||
import { deleteFile, readFile } from '@sasjs/utils'
|
||||
|
||||
import { publishAppStream } from '../appStream'
|
||||
|
||||
import { multerSingle } from '../../middlewares/multer'
|
||||
import { DriveController } from '../../controllers/'
|
||||
import { fileBodyValidation, fileParamValidation } from '../../utils'
|
||||
import {
|
||||
deployValidation,
|
||||
fileBodyValidation,
|
||||
fileParamValidation,
|
||||
folderParamValidation
|
||||
} from '../../utils'
|
||||
|
||||
const controller = new DriveController()
|
||||
|
||||
const driveRouter = express.Router()
|
||||
|
||||
driveRouter.post('/deploy', async (req, res) => {
|
||||
const { error, value: body } = deployValidation(req.body)
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deploy(req.body)
|
||||
const response = await controller.deploy(body)
|
||||
|
||||
if (body.streamWebFolder) {
|
||||
const { streamServiceName } = await publishAppStream(
|
||||
body.appLoc,
|
||||
body.streamWebFolder,
|
||||
body.streamServiceName,
|
||||
body.streamLogo
|
||||
)
|
||||
response.streamServiceName = streamServiceName
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
@@ -22,14 +43,74 @@ driveRouter.post('/deploy', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.post(
|
||||
'/deploy/upload',
|
||||
(...arg) => multerSingle('file', arg),
|
||||
async (req, res) => {
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
|
||||
const fileContent = await readFile(req.file.path)
|
||||
|
||||
let jsonContent
|
||||
try {
|
||||
jsonContent = JSON.parse(fileContent)
|
||||
} catch (err) {
|
||||
deleteFile(req.file.path)
|
||||
return res.status(400).send('File containing invalid JSON content.')
|
||||
}
|
||||
|
||||
const { error, value: body } = deployValidation(jsonContent)
|
||||
if (error) {
|
||||
deleteFile(req.file.path)
|
||||
return res.status(400).send(error.details[0].message)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await controller.deployUpload(req.file, body)
|
||||
|
||||
if (body.streamWebFolder) {
|
||||
const { streamServiceName } = await publishAppStream(
|
||||
body.appLoc,
|
||||
body.streamWebFolder,
|
||||
body.streamServiceName,
|
||||
body.streamLogo
|
||||
)
|
||||
response.streamServiceName = streamServiceName
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
const statusCode = err.code
|
||||
|
||||
delete err.code
|
||||
|
||||
res.status(statusCode).send(err)
|
||||
} finally {
|
||||
deleteFile(req.file.path)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
driveRouter.get('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
await controller.getFile(req, query._filePath, body.filePath)
|
||||
await controller.getFile(req, query._filePath)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
})
|
||||
|
||||
driveRouter.get('/folder', async (req, res) => {
|
||||
const { error: errQ, value: query } = folderParamValidation(req.query)
|
||||
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.getFolder(query._folderPath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
}
|
||||
@@ -37,12 +118,11 @@ driveRouter.get('/file', async (req, res) => {
|
||||
|
||||
driveRouter.delete('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deleteFile(query._filePath, body.filePath)
|
||||
const response = await controller.deleteFile(query._filePath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
@@ -58,7 +138,7 @@ driveRouter.post(
|
||||
|
||||
if (errQ && errB) {
|
||||
if (req.file) await deleteFile(req.file.path)
|
||||
return res.status(400).send(errB.details[0].message)
|
||||
return res.status(400).send(errQ.details[0].message)
|
||||
}
|
||||
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
@@ -86,7 +166,7 @@ driveRouter.patch(
|
||||
|
||||
if (errQ && errB) {
|
||||
if (req.file) await deleteFile(req.file.path)
|
||||
return res.status(400).send(errB.details[0].message)
|
||||
return res.status(400).send(errQ.details[0].message)
|
||||
}
|
||||
|
||||
if (!req.file) return res.status(400).send('"file" is not present.')
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
readFile,
|
||||
deleteFolder,
|
||||
generateTimestamp,
|
||||
copy
|
||||
copy,
|
||||
createFolder,
|
||||
createFile,
|
||||
ServiceMember,
|
||||
FolderMember
|
||||
} from '@sasjs/utils'
|
||||
import * as fileUtilModules from '../../../utils/file'
|
||||
|
||||
@@ -26,7 +30,6 @@ jest
|
||||
import appPromise from '../../../app'
|
||||
import { UserController } from '../../../controllers/'
|
||||
import { getTreeExample } from '../../../controllers/internal'
|
||||
import { FolderMember, ServiceMember } from '../../../types'
|
||||
import { generateAccessToken, saveTokensInDB } from '../../../utils/'
|
||||
const { getTmpFilesFolderPath } = fileUtilModules
|
||||
|
||||
@@ -44,7 +47,7 @@ const user = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
describe('files', () => {
|
||||
describe('drive', () => {
|
||||
let con: Mongoose
|
||||
let mongoServer: MongoMemoryServer
|
||||
const controller = new UserController()
|
||||
@@ -69,19 +72,25 @@ describe('files', () => {
|
||||
await mongoServer.stop()
|
||||
await deleteFolder(tmpFolder)
|
||||
})
|
||||
|
||||
describe('deploy', () => {
|
||||
const shouldFailAssertion = async (payload: any) => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send(payload)
|
||||
.send({ appLoc: '/Public', fileTree: payload })
|
||||
|
||||
expect(res.statusCode).toEqual(400)
|
||||
expect(res.body).toEqual({
|
||||
status: 'failure',
|
||||
message: 'Provided not supported data format.',
|
||||
example: getTreeExample()
|
||||
})
|
||||
|
||||
if (payload === undefined) {
|
||||
expect(res.text).toEqual('"fileTree" is required')
|
||||
} else {
|
||||
expect(res.body).toEqual({
|
||||
status: 'failure',
|
||||
message: 'Provided not supported data format.',
|
||||
example: getTreeExample()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
it('should respond with payload example if valid payload was not provided', async () => {
|
||||
@@ -140,11 +149,11 @@ describe('files', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should respond with payload example if valid payload was not provided', async () => {
|
||||
it('should successfully deploy if valid payload was provided', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/deploy')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.send({ fileTree: getTreeExample() })
|
||||
.send({ appLoc: '/public', fileTree: getTreeExample() })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.text).toEqual(
|
||||
@@ -154,6 +163,7 @@ describe('files', () => {
|
||||
|
||||
const testJobFolder = path.join(
|
||||
getTmpFilesFolderPath(),
|
||||
'public',
|
||||
'jobs',
|
||||
'extract'
|
||||
)
|
||||
@@ -166,17 +176,126 @@ describe('files', () => {
|
||||
|
||||
await expect(readFile(testJobFile)).resolves.toEqual(exampleService.code)
|
||||
|
||||
await deleteFolder(getTmpFilesFolderPath())
|
||||
await deleteFolder(path.join(getTmpFilesFolderPath(), 'public'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('folder', () => {
|
||||
describe('get', () => {
|
||||
const getFolderApi = '/SASjsApi/drive/folder'
|
||||
|
||||
it('should get root SAS folder on drive', async () => {
|
||||
const res = await request(app)
|
||||
.get(getFolderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({ files: [], folders: [] })
|
||||
})
|
||||
|
||||
it('should get a SAS folder on drive having _folderPath as query param', async () => {
|
||||
const pathToDrive = fileUtilModules.getTmpFilesFolderPath()
|
||||
|
||||
const dirLevel1 = 'level1'
|
||||
const dirLevel2 = 'level2'
|
||||
const fileLevel1 = 'file1'
|
||||
const fileLevel2 = 'file2'
|
||||
|
||||
await createFolder(path.join(pathToDrive, dirLevel1, dirLevel2))
|
||||
await createFile(
|
||||
path.join(pathToDrive, dirLevel1, fileLevel1),
|
||||
'some file content'
|
||||
)
|
||||
await createFile(
|
||||
path.join(pathToDrive, dirLevel1, dirLevel2, fileLevel2),
|
||||
'some file content'
|
||||
)
|
||||
|
||||
const res1 = await request(app)
|
||||
.get(getFolderApi)
|
||||
.query({ _folderPath: '/' })
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
|
||||
expect(res1.statusCode).toEqual(200)
|
||||
expect(res1.body).toEqual({ files: [], folders: [dirLevel1] })
|
||||
|
||||
const res2 = await request(app)
|
||||
.get(getFolderApi)
|
||||
.query({ _folderPath: dirLevel1 })
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
|
||||
expect(res2.statusCode).toEqual(200)
|
||||
expect(res2.body).toEqual({ files: [fileLevel1], folders: [dirLevel2] })
|
||||
|
||||
const res3 = await request(app)
|
||||
.get(getFolderApi)
|
||||
.query({ _folderPath: `${dirLevel1}/${dirLevel2}` })
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
|
||||
expect(res3.statusCode).toEqual(200)
|
||||
expect(res3.body).toEqual({ files: [fileLevel2], folders: [] })
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app).get(getFolderApi).expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if folder is not present', async () => {
|
||||
const res = await request(app)
|
||||
.get(getFolderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: `/my/path/code-${generateTimestamp()}` })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual(`Error: Folder doesn't exist.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if folderPath outside Drive', async () => {
|
||||
const res = await request(app)
|
||||
.get(getFolderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: '/../path/code.sas' })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot get folder outside drive.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if folderPath is of a file', async () => {
|
||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const filePath = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
filePath
|
||||
)
|
||||
await copy(fileToCopyPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
.get(getFolderApi)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _folderPath: filePath })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Not a Folder.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('file', () => {
|
||||
describe('create', () => {
|
||||
it('should create a SAS file on drive having filePath as form field', async () => {
|
||||
const pathToUpload = `/my/path/code-1.sas`
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', '/my/path/code.sas')
|
||||
.field('filePath', pathToUpload)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
@@ -186,10 +305,12 @@ describe('files', () => {
|
||||
})
|
||||
|
||||
it('should create a SAS file on drive having _filePath as query param', async () => {
|
||||
const pathToUpload = `/my/path/code-2.sas`
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: '/my/path/code1.sas' })
|
||||
.query({ _filePath: pathToUpload })
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
@@ -211,7 +332,7 @@ describe('files', () => {
|
||||
|
||||
it('should respond with Forbidden if file is already present', async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
const pathToUpload = `/my/path/code-${generateTimestamp()}.sas`
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
@@ -254,22 +375,22 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"filePath" is required`)
|
||||
expect(res.text).toEqual(`"_filePath" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.oth'
|
||||
const pathToUpload = '/my/path/code.exe'
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
// .field('filePath', pathToUpload)
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -287,7 +408,7 @@ describe('files', () => {
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
@@ -297,16 +418,14 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||
)
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
@@ -316,7 +435,7 @@ describe('files', () => {
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
@@ -382,7 +501,7 @@ describe('files', () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', `/my/path/code-${generateTimestamp()}.sas`)
|
||||
.field('filePath', `/my/path/code-3.sas`)
|
||||
.attach('file', path.join(__dirname, 'files', 'sample.sas'))
|
||||
.expect(403)
|
||||
|
||||
@@ -414,22 +533,22 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"filePath" is required`)
|
||||
expect(res.text).toEqual(`"_filePath" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const pathToUpload = '/my/path/code.oth'
|
||||
const pathToUpload = '/my/path/code.exe'
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.field('filePath', pathToUpload)
|
||||
.query({ _filePath: pathToUpload })
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -447,7 +566,7 @@ describe('files', () => {
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
@@ -457,16 +576,14 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||
)
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if attached file exceeds file limit', async () => {
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const attachedFile = Buffer.from('.'.repeat(20 * 1024 * 1024))
|
||||
const attachedFile = Buffer.from('.'.repeat(110 * 1024 * 1024)) // 110mb
|
||||
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
@@ -476,11 +593,84 @@ describe('files', () => {
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
'File size is over limit. File limit is: 10 MB'
|
||||
'File size is over limit. File limit is: 100 MB'
|
||||
)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('get', () => {
|
||||
it('should get a SAS file on drive having _filePath as query param', async () => {
|
||||
const fileToCopyPath = path.join(__dirname, 'files', 'sample.sas')
|
||||
const fileToCopyContent = await readFile(fileToCopyPath)
|
||||
const filePath = '/my/path/code.sas'
|
||||
|
||||
const pathToCopy = path.join(
|
||||
fileUtilModules.getTmpFilesFolderPath(),
|
||||
filePath
|
||||
)
|
||||
await copy(fileToCopyPath, pathToCopy)
|
||||
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: filePath })
|
||||
|
||||
expect(res.statusCode).toEqual(200)
|
||||
expect(res.body).toEqual({})
|
||||
expect(res.text).toEqual(fileToCopyContent)
|
||||
})
|
||||
|
||||
it('should respond with Unauthorized if access token is not present', async () => {
|
||||
const res = await request(app).get('/SASjsApi/drive/file').expect(401)
|
||||
|
||||
expect(res.text).toEqual('Unauthorized')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if file is not present', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: `/my/path/code-4.sas` })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual(`Error: File doesn't exist.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Forbidden if filePath outside Drive', async () => {
|
||||
const res = await request(app)
|
||||
.get('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: '/../path/code.sas' })
|
||||
.expect(403)
|
||||
|
||||
expect(res.text).toEqual('Error: Cannot get file outside drive.')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
const res = await request(app)
|
||||
.patch('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.query({ _filePath: '/my/path/code.exe' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
it('should respond with Bad Request if filePath is missing', async () => {
|
||||
const res = await request(app)
|
||||
.post('/SASjsApi/drive/file')
|
||||
.auth(accessToken, { type: 'bearer' })
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(`"_filePath" is required`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
44
api/src/routes/appStream/appStreamHtml.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { AppStreamConfig } from '../../types'
|
||||
import { script } from './script'
|
||||
import { style } from './style'
|
||||
|
||||
const defaultAppLogo = '/sasjs-logo.svg'
|
||||
|
||||
const singleAppStreamHtml = (
|
||||
streamServiceName: string,
|
||||
appLoc: string,
|
||||
logo?: string
|
||||
) =>
|
||||
` <a class="app" href="${streamServiceName}" title="${appLoc}">
|
||||
<img
|
||||
src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}"
|
||||
onerror="this.src = '${defaultAppLogo}';"
|
||||
/>
|
||||
${streamServiceName}
|
||||
</a>`
|
||||
|
||||
export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
||||
<html>
|
||||
<head>
|
||||
<base href="/AppStream/">
|
||||
${style}
|
||||
</head>
|
||||
<body>
|
||||
<h1>App Stream</h1>
|
||||
<div class="app-container">
|
||||
${Object.entries(appStreamConfig)
|
||||
.map(([streamServiceName, entry]) =>
|
||||
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
||||
)
|
||||
.join('')}
|
||||
<a class="app" title="Upload build.json">
|
||||
<input id="fileId" type="file" hidden />
|
||||
<button id="uploadButton" style="margin-bottom: 5px; cursor: pointer">
|
||||
<img src="/plus.png" />
|
||||
</button>
|
||||
<span id="uploadMessage">Upload New App</span>
|
||||
</a>
|
||||
</div>
|
||||
${script}
|
||||
</body>
|
||||
</html>`
|
||||
65
api/src/routes/appStream/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import { folderExists } from '@sasjs/utils'
|
||||
|
||||
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
|
||||
import { appStreamHtml } from './appStreamHtml'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async (_, res) => {
|
||||
const content = appStreamHtml(process.appStreamConfig)
|
||||
|
||||
return res.send(content)
|
||||
})
|
||||
|
||||
export const publishAppStream = async (
|
||||
appLoc: string,
|
||||
streamWebFolder: string,
|
||||
streamServiceName?: string,
|
||||
streamLogo?: string,
|
||||
addEntryToFile: boolean = true
|
||||
) => {
|
||||
const driveFilesPath = getTmpFilesFolderPath()
|
||||
|
||||
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
|
||||
const appLocPath = path.join(driveFilesPath, ...appLocParts)
|
||||
if (!appLocPath.includes(driveFilesPath)) {
|
||||
throw new Error('appLoc cannot be outside drive.')
|
||||
}
|
||||
|
||||
const pathToDeployment = path.join(appLocPath, 'services', streamWebFolder)
|
||||
if (!pathToDeployment.includes(appLocPath)) {
|
||||
throw new Error('streamWebFolder cannot be outside appLoc.')
|
||||
}
|
||||
|
||||
if (await folderExists(pathToDeployment)) {
|
||||
const appCount = process.appStreamConfig
|
||||
? Object.keys(process.appStreamConfig).length
|
||||
: 0
|
||||
|
||||
if (!streamServiceName) {
|
||||
streamServiceName = `AppStreamName${appCount + 1}`
|
||||
}
|
||||
|
||||
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
|
||||
|
||||
addEntryToAppStreamConfig(
|
||||
streamServiceName,
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamLogo,
|
||||
addEntryToFile
|
||||
)
|
||||
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
console.log(
|
||||
'Serving Stream App: ',
|
||||
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
|
||||
)
|
||||
return { streamServiceName }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export default router
|
||||
58
api/src/routes/appStream/script.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export const script = `<script>
|
||||
const inputElement = document.getElementById('fileId')
|
||||
|
||||
document
|
||||
.getElementById('uploadButton')
|
||||
.addEventListener('click', function () {
|
||||
inputElement.click()
|
||||
})
|
||||
|
||||
inputElement.addEventListener(
|
||||
'change',
|
||||
function () {
|
||||
const fileList = this.files /* now you can work with the file list */
|
||||
|
||||
updateFileUploadMessage('Requesting ...')
|
||||
|
||||
const file = fileList[0]
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', file)
|
||||
fetch('/SASjsApi/drive/deploy/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(async (res) => {
|
||||
const { status, ok } = res
|
||||
if (status === 200 && ok) {
|
||||
const data = await res.json()
|
||||
return (
|
||||
data.message +
|
||||
'\\nstreamServiceName: ' +
|
||||
data.streamServiceName +
|
||||
'\\nrefreshing page once alert box closes.'
|
||||
)
|
||||
}
|
||||
throw await res.text()
|
||||
})
|
||||
.then((message) => {
|
||||
alert(message)
|
||||
location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(error)
|
||||
resetFileUpload()
|
||||
updateFileUploadMessage('Upload New App')
|
||||
})
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
function updateFileUploadMessage(message) {
|
||||
document.getElementById('uploadMessage').innerHTML = message
|
||||
}
|
||||
|
||||
function resetFileUpload() {
|
||||
inputElement.value = null
|
||||
}
|
||||
</script>`
|
||||
22
api/src/routes/appStream/style.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const style = `<style>
|
||||
* {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
.app-container .app {
|
||||
width: 150px;
|
||||
margin: 10px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>`
|
||||
@@ -2,8 +2,15 @@ import { Express } from 'express'
|
||||
|
||||
import webRouter from './web'
|
||||
import apiRouter from './api'
|
||||
import appStreamRouter from './appStream'
|
||||
|
||||
export const setupRoutes = (app: Express) => {
|
||||
app.use('/', webRouter)
|
||||
app.use('/SASjsApi', apiRouter)
|
||||
|
||||
app.use('/AppStream', function (req, res, next) {
|
||||
// this needs to be a function to hook on
|
||||
// whatever the current router is
|
||||
appStreamRouter(req, res, next)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import appPromise from './app'
|
||||
import { getCertificates } from './utils'
|
||||
|
||||
appPromise.then(async (app) => {
|
||||
const protocol = process.env.PROTOCOL ?? 'http'
|
||||
const sasJsPort = process.env.PORT ?? 5000
|
||||
const protocol = process.env.PROTOCOL || 'http'
|
||||
const sasJsPort = process.env.PORT || 5000
|
||||
|
||||
console.log('PROTOCOL: ', protocol)
|
||||
|
||||
|
||||
7
api/src/types/AppStreamConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface AppStreamConfig {
|
||||
[key: string]: {
|
||||
appLoc: string
|
||||
streamWebFolder: string
|
||||
streamLogo?: string
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
export interface FileTree {
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
}
|
||||
|
||||
export enum MemberType {
|
||||
folder = 'folder',
|
||||
service = 'service'
|
||||
}
|
||||
|
||||
export interface FolderMember {
|
||||
name: string
|
||||
type: MemberType.folder
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
}
|
||||
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service
|
||||
code: string
|
||||
}
|
||||
|
||||
export const isFileTree = (arg: any): arg is FileTree =>
|
||||
arg &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
).length === 0
|
||||
|
||||
const isFolderMember = (arg: any): arg is FolderMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.folder &&
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
).length === 0
|
||||
|
||||
const isServiceMember = (arg: any): arg is ServiceMember =>
|
||||
arg &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.service &&
|
||||
arg.code &&
|
||||
typeof arg.code === 'string'
|
||||
1
api/src/types/Process.d.ts
vendored
@@ -3,5 +3,6 @@ declare namespace NodeJS {
|
||||
sasLoc: string
|
||||
driveLoc: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
appStreamConfig: import('./').AppStreamConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// TODO: uppercase types
|
||||
export * from './AppStreamConfig'
|
||||
export * from './Execution'
|
||||
export * from './FileTree'
|
||||
export * from './InfoJWT'
|
||||
export * from './PreProgramVars'
|
||||
export * from './Request'
|
||||
|
||||
89
api/src/utils/appStreamConfig.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { createFile, fileExists, readFile } from '@sasjs/utils'
|
||||
import { publishAppStream } from '../routes/appStream'
|
||||
import { AppStreamConfig } from '../types'
|
||||
|
||||
import { getTmpAppStreamConfigPath } from './file'
|
||||
|
||||
export const loadAppStreamConfig = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||
|
||||
const content = (await fileExists(appStreamConfigPath))
|
||||
? await readFile(appStreamConfigPath)
|
||||
: '{}'
|
||||
|
||||
let appStreamConfig: AppStreamConfig
|
||||
try {
|
||||
appStreamConfig = JSON.parse(content)
|
||||
|
||||
if (!isValidAppStreamConfig(appStreamConfig)) throw 'invalid type'
|
||||
} catch (_) {
|
||||
appStreamConfig = {}
|
||||
}
|
||||
process.appStreamConfig = {}
|
||||
|
||||
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
|
||||
const { appLoc, streamWebFolder, streamLogo } = entry
|
||||
|
||||
publishAppStream(
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamServiceName,
|
||||
streamLogo,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
console.log('App Stream Config loaded!')
|
||||
}
|
||||
|
||||
export const addEntryToAppStreamConfig = (
|
||||
streamServiceName: string,
|
||||
appLoc: string,
|
||||
streamWebFolder: string,
|
||||
streamLogo?: string,
|
||||
addEntryToFile: boolean = true
|
||||
) => {
|
||||
if (streamServiceName && appLoc && streamWebFolder) {
|
||||
process.appStreamConfig[streamServiceName] = {
|
||||
appLoc,
|
||||
streamWebFolder,
|
||||
streamLogo
|
||||
}
|
||||
if (addEntryToFile) saveAppStreamConfig()
|
||||
}
|
||||
}
|
||||
|
||||
export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
|
||||
if (streamServiceName) {
|
||||
delete process.appStreamConfig[streamServiceName]
|
||||
saveAppStreamConfig()
|
||||
}
|
||||
}
|
||||
|
||||
const saveAppStreamConfig = async () => {
|
||||
const appStreamConfigPath = getTmpAppStreamConfigPath()
|
||||
|
||||
try {
|
||||
await createFile(
|
||||
appStreamConfigPath,
|
||||
JSON.stringify(process.appStreamConfig, null, 2)
|
||||
)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const isValidAppStreamConfig = (config: any) => {
|
||||
if (config) {
|
||||
return !Object.entries(config).some(([streamServiceName, entry]) => {
|
||||
const { appLoc, streamWebFolder, streamLogo } = entry as any
|
||||
|
||||
return (
|
||||
typeof streamServiceName !== 'string' ||
|
||||
typeof appLoc !== 'string' ||
|
||||
typeof streamWebFolder !== 'string'
|
||||
)
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
34
api/src/utils/copySASjsCore.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
asyncForEach,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||
|
||||
export const copySASjsCore = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
console.log('Copying Macros from container to drive(tmp).')
|
||||
|
||||
const macrosDrivePath = getTmpMacrosPath()
|
||||
|
||||
await deleteFolder(macrosDrivePath)
|
||||
await createFolder(macrosDrivePath)
|
||||
|
||||
const macros = await readFile(sasJSCoreMacrosInfo)
|
||||
|
||||
await asyncForEach(macros.split('\n'), async (macroName) => {
|
||||
const macroFileSourcePath = path.join(sasJSCoreMacros, macroName)
|
||||
const macroContent = await readFile(macroFileSourcePath)
|
||||
|
||||
const macroFileDestPath = path.join(macrosDrivePath, macroName)
|
||||
|
||||
await createFile(macroFileDestPath, macroContent)
|
||||
})
|
||||
|
||||
console.log('Macros Drive Path:', macrosDrivePath)
|
||||
}
|
||||
@@ -9,12 +9,18 @@ export const sysInitCompiledPath = path.join(
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
|
||||
|
||||
export const getWebBuildFolderPath = () =>
|
||||
path.join(codebaseRoot, 'web', 'build')
|
||||
|
||||
export const getTmpFolderPath = () => process.driveLoc
|
||||
|
||||
export const getTmpAppStreamConfigPath = () =>
|
||||
path.join(getTmpFolderPath(), 'appStreamConfig.json')
|
||||
|
||||
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
|
||||
|
||||
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
|
||||
|
||||
export const getTmpFilesFolderPath = () =>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
export * from './appStreamConfig'
|
||||
export * from './connectDB'
|
||||
export * from './copySASjsCore'
|
||||
export * from './extractHeaders'
|
||||
export * from './file'
|
||||
export * from './generateAccessToken'
|
||||
export * from './generateAuthCode'
|
||||
export * from './generateRefreshToken'
|
||||
export * from './isDebugOn'
|
||||
export * from './getCertificates'
|
||||
export * from './getDesktopFields'
|
||||
export * from './isDebugOn'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
export * from './setProcessVariables'
|
||||
export * from './sleep'
|
||||
export * from './setupFolders'
|
||||
export * from './upload'
|
||||
export * from './validation'
|
||||
export * from './verifyTokenInDB'
|
||||
|
||||
7
api/src/utils/setupFolders.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createFolder } from '@sasjs/utils'
|
||||
import { getTmpFilesFolderPath } from './file'
|
||||
|
||||
export const setupFolders = async () => {
|
||||
const drivePath = getTmpFilesFolderPath()
|
||||
await createFolder(drivePath)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const sleep = async (delay: number) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { getTmpSessionsFolderPath } from '.'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
import { listFilesInFolder } from '@sasjs/utils'
|
||||
|
||||
interface FilenameMapSingle {
|
||||
fieldName: string
|
||||
originalName: string
|
||||
}
|
||||
|
||||
interface FilenamesMap {
|
||||
[key: string]: FilenameMapSingle
|
||||
}
|
||||
|
||||
interface UploadedFiles extends FilenameMapSingle {
|
||||
fileref: string
|
||||
filepath: string
|
||||
count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* It will create an object that maps hashed file names to the original names
|
||||
* @param files array of files to be mapped
|
||||
@@ -12,10 +24,13 @@ import { listFilesInFolder } from '@sasjs/utils'
|
||||
export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||
if (!files) return null
|
||||
|
||||
const filesNamesMap: { [key: string]: string } = {}
|
||||
const filesNamesMap: FilenamesMap = {}
|
||||
|
||||
for (let file of files) {
|
||||
filesNamesMap[file.filename] = file.fieldname
|
||||
filesNamesMap[file.filename] = {
|
||||
fieldName: file.fieldname,
|
||||
originalName: file.originalname
|
||||
}
|
||||
}
|
||||
|
||||
return filesNamesMap
|
||||
@@ -28,17 +43,12 @@ export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||
* @returns generated sas code
|
||||
*/
|
||||
export const generateFileUploadSasCode = async (
|
||||
filesNamesMap: any,
|
||||
filesNamesMap: FilenamesMap,
|
||||
sasSessionFolder: string
|
||||
): Promise<string> => {
|
||||
let uploadSasCode = ''
|
||||
let fileCount = 0
|
||||
let uploadedFilesMap: {
|
||||
fileref: string
|
||||
filepath: string
|
||||
filename: string
|
||||
count: number
|
||||
}[] = []
|
||||
const uploadedFiles: UploadedFiles[] = []
|
||||
|
||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||
sasSessionFolder
|
||||
@@ -50,31 +60,32 @@ export const generateFileUploadSasCode = async (
|
||||
if (fileName.includes('req_file')) {
|
||||
fileCount++
|
||||
|
||||
uploadedFilesMap.push({
|
||||
uploadedFiles.push({
|
||||
fileref: `_sjs${fileCountString}`,
|
||||
filepath: `${sasSessionFolder}/${fileName}`,
|
||||
filename: filesNamesMap[fileName],
|
||||
originalName: filesNamesMap[fileName].originalName,
|
||||
fieldName: filesNamesMap[fileName].fieldName,
|
||||
count: fileCount
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\nfilename ${uploadedFile.fileref} "${uploadedFile.filepath}";`
|
||||
}
|
||||
|
||||
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filepath};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedFile.count}=${uploadedFile.originalName};`
|
||||
}
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedFile.count}=${uploadedFile.fileref};`
|
||||
}
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filename};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedFile.count}=${uploadedFile.fieldName};`
|
||||
}
|
||||
|
||||
if (fileCount > 0) {
|
||||
|
||||
@@ -3,6 +3,8 @@ import Joi from 'joi'
|
||||
const usernameSchema = Joi.string().alphanum().min(6).max(20)
|
||||
const passwordSchema = Joi.string().min(6).max(1024)
|
||||
|
||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||
|
||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
username: usernameSchema.required(),
|
||||
@@ -66,18 +68,39 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||
clientSecret: Joi.string().required()
|
||||
}).validate(data)
|
||||
|
||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||
streamServiceName: Joi.string(),
|
||||
streamWebFolder: Joi.string(),
|
||||
streamLogo: Joi.string(),
|
||||
fileTree: Joi.any().required()
|
||||
}).validate(data)
|
||||
|
||||
const filePathSchema = Joi.string()
|
||||
.custom((value, helpers) => {
|
||||
if (blockFileRegex.test(value)) return helpers.error('string.pattern.base')
|
||||
|
||||
return value
|
||||
})
|
||||
.required()
|
||||
.messages({
|
||||
'string.pattern.base': `Invalid file extension`
|
||||
})
|
||||
|
||||
export const fileBodyValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||
})
|
||||
filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||
})
|
||||
_filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const folderParamValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_folderPath: Joi.string()
|
||||
}).validate(data)
|
||||
|
||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
32
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.51",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.51",
|
||||
"devDependencies": {
|
||||
"prettier": "^2.3.1",
|
||||
"standard-version": "^9.3.2"
|
||||
@@ -404,14 +404,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/conventional-changelog-writer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz",
|
||||
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
|
||||
"integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"conventional-commits-filter": "^2.0.7",
|
||||
"dateformat": "^3.0.0",
|
||||
"handlebars": "^4.7.6",
|
||||
"handlebars": "^4.7.7",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"meow": "^8.0.0",
|
||||
@@ -1350,9 +1350,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimist-options": {
|
||||
@@ -2433,14 +2433,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"conventional-changelog-writer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz",
|
||||
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
|
||||
"integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"conventional-commits-filter": "^2.0.7",
|
||||
"dateformat": "^3.0.0",
|
||||
"handlebars": "^4.7.6",
|
||||
"handlebars": "^4.7.7",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"meow": "^8.0.0",
|
||||
@@ -3158,9 +3158,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist-options": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.32",
|
||||
"version": "0.0.51",
|
||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||
"repository": "https://github.com/sasjs/server",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
### Get contents of folder
|
||||
GET http://localhost:5000/SASjsApi/drive/folder?_path=/Public/app/react-seed-app/services/web
|
||||
|
||||
###
|
||||
POST http://localhost:5000/SASjsApi/drive/deploy
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJjbGllbnRJRDEiLCJ1c2VybmFtZSI6InVzZXJuYW1lMSIsImlzYWRtaW4iOmZhbHNlLCJpc2FjdGl2ZSI6dHJ1ZSwiaWF0IjoxNjM1ODA0MDc2LCJleHAiOjE2MzU4OTA0NzZ9.Cx1F54ILgAUtnkit0Wg1K1YVO2RdNjOnTKdPhUtDm5I
|
||||
|
||||
25
restClient/stp.rest
Normal file
@@ -0,0 +1,25 @@
|
||||
### testing upload file example
|
||||
POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
Content-Disposition: form-data; name="fileSome11"; filename="DCCONFIG.MPE_X_TEST.xlsx"
|
||||
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
|
||||
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
Content-Disposition: form-data; name="fileSome22"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv"
|
||||
Content-Type: application/csv
|
||||
|
||||
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM
|
||||
,0,this is dummy data 321,Option 1,42,1960-02-12,1960-01-01 00:00:42,00:00:42,3,44
|
||||
,1,more dummy data 123,Option 2,42,1960-02-12,1960-01-01 00:00:42,00:07:02,3,44
|
||||
,1039,39 bottles of beer on the wall,Option 1,0.8716847965827607,1962-05-30,1960-01-01 00:05:21,00:01:30,89,6
|
||||
,1045,45 bottles of beer on the wall,Option 1,0.7279699667021492,1960-03-24,1960-01-01 07:18:54,00:01:08,89,83
|
||||
,1047,47 bottles of beer on the wall,Option 1,0.6224654082313484,1961-06-07,1960-01-01 09:45:23,00:01:33,76,98
|
||||
,1048,48 bottles of beer on the wall,Option 1,0.0874847523344144,1962-03-01,1960-01-01 13:06:13,00:00:02,76,63
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy
|
||||
Content-Disposition: form-data; name="_debug"
|
||||
|
||||
131
|
||||
------WebKitFormBoundarynkYOqevUMKZrXeAy--
|
||||
1
web/.nvmrc
Normal file
@@ -0,0 +1 @@
|
||||
v16.14.0
|
||||
41
web/package-lock.json
generated
@@ -8472,9 +8472,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
@@ -8581,9 +8581,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
|
||||
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
@@ -9215,11 +9215,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
|
||||
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==",
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
|
||||
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
@@ -17619,9 +17622,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
@@ -17712,9 +17715,9 @@
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
|
||||
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
|
||||
"dev": true
|
||||
},
|
||||
"node-releases": {
|
||||
@@ -18181,9 +18184,9 @@
|
||||
}
|
||||
},
|
||||
"prismjs": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.25.0.tgz",
|
||||
"integrity": "sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==",
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
|
||||
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
web/public/running-sas.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -5,6 +5,13 @@ import AppBar from '@mui/material/AppBar'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import Tabs from '@mui/material/Tabs'
|
||||
import Tab from '@mui/material/Tab'
|
||||
import Button from '@mui/material/Button'
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV
|
||||
const PORT_API = process.env.PORT_API
|
||||
const baseUrl =
|
||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||
|
||||
const Header = (props: any) => {
|
||||
const history = useHistory()
|
||||
@@ -21,10 +28,10 @@ const Header = (props: any) => {
|
||||
>
|
||||
<Toolbar variant="dense">
|
||||
<img
|
||||
src="logo-white.png"
|
||||
src="logo.png"
|
||||
alt="logo"
|
||||
style={{
|
||||
width: '50px',
|
||||
width: '35px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '25px'
|
||||
}}
|
||||
@@ -52,6 +59,28 @@ const Header = (props: any) => {
|
||||
component={Link}
|
||||
/>
|
||||
</Tabs>
|
||||
<Button
|
||||
href={`${baseUrl}/SASjsApi`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
API Docs
|
||||
</Button>
|
||||
<Button
|
||||
href={`${baseUrl}/AppStream`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
App Stream
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import Box from '@mui/material/Box'
|
||||
@@ -6,13 +8,93 @@ import Box from '@mui/material/Box'
|
||||
import SideBar from './sideBar'
|
||||
import Main from './main'
|
||||
|
||||
export interface TreeNode {
|
||||
name: string
|
||||
relativePath: string
|
||||
absolutePath: string
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
|
||||
const Drive = () => {
|
||||
const location = useLocation()
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
const [selectedFilePath, setSelectedFilePath] = useState('')
|
||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||
|
||||
const setFilePathOnMount = useCallback(() => {
|
||||
const queryParams = new URLSearchParams(location.search)
|
||||
setSelectedFilePath(queryParams.get('filePath') ?? '')
|
||||
}, [location.search])
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`/SASjsApi/drive/fileTree`)
|
||||
.then((res: any) => {
|
||||
if (res.data && res.data?.status === 'success') {
|
||||
setDirectoryData(res.data.tree)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
setFilePathOnMount()
|
||||
}, [setFilePathOnMount])
|
||||
|
||||
const handleSelect = (node: TreeNode) => {
|
||||
if (node.children.length) return
|
||||
|
||||
if (!node.name.includes('.')) return
|
||||
|
||||
window.history.pushState(
|
||||
'',
|
||||
'',
|
||||
`${baseUrl}/#/SASjsDrive?filePath=${node.relativePath}`
|
||||
)
|
||||
setSelectedFilePath(node.relativePath)
|
||||
}
|
||||
|
||||
const removeFileFromTree = (path: string) => {
|
||||
if (directoryData) {
|
||||
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
|
||||
findAndRemoveNode(newTree, newTree, path)
|
||||
setDirectoryData(newTree)
|
||||
}
|
||||
}
|
||||
|
||||
const findAndRemoveNode = (
|
||||
node: TreeNode,
|
||||
parentNode: TreeNode,
|
||||
path: string
|
||||
) => {
|
||||
if (node.relativePath === path) {
|
||||
removeNodeFromParent(parentNode, path)
|
||||
return true
|
||||
}
|
||||
if (Array.isArray(node.children)) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
if (findAndRemoveNode(node.children[i], node, path)) return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeNodeFromParent = (parent: TreeNode, path: string) => {
|
||||
const index = parent.children.findIndex(
|
||||
(node) => node.relativePath === path
|
||||
)
|
||||
if (index !== -1) {
|
||||
parent.children.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
<SideBar setSelectedFilePath={setSelectedFilePath} />
|
||||
<Main selectedFilePath={selectedFilePath} />
|
||||
<SideBar directoryData={directoryData} handleSelect={handleSelect} />
|
||||
<Main
|
||||
selectedFilePath={selectedFilePath}
|
||||
removeFileFromTree={removeFileFromTree}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,12 @@ import Button from '@mui/material/Button'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
const Main = (props: any) => {
|
||||
type Props = {
|
||||
selectedFilePath: string
|
||||
removeFileFromTree: (path: string) => void
|
||||
}
|
||||
|
||||
const Main = (props: Props) => {
|
||||
const baseUrl = window.location.origin
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
@@ -45,6 +50,7 @@ const Main = (props: any) => {
|
||||
.delete(`/SASjsApi/drive/file?_filePath=${filePath}`)
|
||||
.then((res) => {
|
||||
setFileContent('')
|
||||
props.removeFileFromTree(filePath)
|
||||
window.history.pushState('', '', `${baseUrl}/#/SASjsDrive`)
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import axios from 'axios'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import React from 'react'
|
||||
|
||||
import { makeStyles } from '@mui/styles'
|
||||
|
||||
@@ -16,12 +14,7 @@ import TreeItem from '@mui/lab/TreeItem'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||
|
||||
interface TreeNode {
|
||||
name: string
|
||||
relativePath: string
|
||||
absolutePath: string
|
||||
children: Array<TreeNode>
|
||||
}
|
||||
import { TreeNode } from '.'
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
@@ -36,46 +29,14 @@ const useStyles = makeStyles(() => ({
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
const SideBar = (props: any) => {
|
||||
const location = useLocation()
|
||||
const baseUrl = window.location.origin
|
||||
type Props = {
|
||||
directoryData: TreeNode | null
|
||||
handleSelect: (node: TreeNode) => void
|
||||
}
|
||||
|
||||
const SideBar = ({ directoryData, handleSelect }: Props) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { setSelectedFilePath } = props
|
||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||
|
||||
const setFilePathOnMount = useCallback(() => {
|
||||
const queryParams = new URLSearchParams(location.search)
|
||||
setSelectedFilePath(queryParams.get('filePath'))
|
||||
}, [location.search, setSelectedFilePath])
|
||||
|
||||
useEffect(() => {
|
||||
axios
|
||||
.get(`/SASjsApi/drive/fileTree`)
|
||||
.then((res: any) => {
|
||||
if (res.data && res.data?.status === 'success') {
|
||||
setDirectoryData(res.data.tree)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
setFilePathOnMount()
|
||||
}, [setFilePathOnMount])
|
||||
|
||||
const handleSelect = (node: TreeNode) => {
|
||||
if (node.children.length) return
|
||||
|
||||
if (!node.name.includes('.')) return
|
||||
|
||||
window.history.pushState(
|
||||
'',
|
||||
'',
|
||||
`${baseUrl}/#/SASjsDrive?filePath=${node.relativePath}`
|
||||
)
|
||||
setSelectedFilePath(node.relativePath)
|
||||
}
|
||||
|
||||
const renderTree = (nodes: TreeNode) => (
|
||||
<TreeItem
|
||||
classes={{ root: classes.root }}
|
||||
@@ -107,12 +68,15 @@ const SideBar = (props: any) => {
|
||||
>
|
||||
<Toolbar />
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
>
|
||||
{directoryData && renderTree(directoryData)}
|
||||
</TreeView>
|
||||
{directoryData && (
|
||||
<TreeView
|
||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||
defaultExpandIcon={<ChevronRightIcon />}
|
||||
defaultExpanded={[directoryData.relativePath]}
|
||||
>
|
||||
{renderTree(directoryData)}
|
||||
</TreeView>
|
||||
)}
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
import Box from '@mui/material/Box'
|
||||
import { Button, Paper, Stack, Tab } from '@mui/material'
|
||||
import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material'
|
||||
import { makeStyles } from '@mui/styles'
|
||||
import Editor, { OnMount } from '@monaco-editor/react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@@ -15,6 +15,17 @@ const useStyles = makeStyles(() => ({
|
||||
'&.Mui-selected': {
|
||||
color: 'black'
|
||||
}
|
||||
},
|
||||
subMenu: {
|
||||
marginTop: '25px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
runButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '5px 5px',
|
||||
minWidth: 'unset'
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -22,8 +33,10 @@ const Studio = () => {
|
||||
const location = useLocation()
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||
const [webout, setWebout] = useState('')
|
||||
const [tab, setTab] = React.useState('1')
|
||||
|
||||
const handleTabChange = (_e: any, newValue: string) => {
|
||||
setTab(newValue)
|
||||
}
|
||||
@@ -50,30 +63,32 @@ const Studio = () => {
|
||||
.map((logLine: any) => logLine.line)
|
||||
.join('\n')
|
||||
|
||||
setLog(`<div><h2>SAS Log</h2><pre>${parsedLog}</pre></div>`)
|
||||
setLog(parsedLog)
|
||||
|
||||
let weboutString: string
|
||||
try {
|
||||
weboutString = res.data.webout
|
||||
.split('>>weboutBEGIN<<')[1]
|
||||
.split('>>weboutEND<<')[0]
|
||||
} catch (_) {
|
||||
weboutString = res?.data?.webout ?? ''
|
||||
}
|
||||
|
||||
let webout: string
|
||||
try {
|
||||
webout = JSON.stringify(JSON.parse(weboutString), null, 4)
|
||||
} catch (_) {
|
||||
webout = weboutString
|
||||
}
|
||||
|
||||
setWebout(`<pre><code>${webout}</code></pre>`)
|
||||
setWebout(`${res.data?._webout}`)
|
||||
setTab('2')
|
||||
|
||||
// Scroll to bottom of log
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const content = localStorage.getItem('fileContent') ?? ''
|
||||
setFileContent(content)
|
||||
@@ -97,73 +112,89 @@ const Studio = () => {
|
||||
}, [location.search])
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<Box sx={{ width: '100%', typography: 'body1' }}>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<Tab className={classes.root} label="Code" value="1" />
|
||||
<Tab className={classes.root} label="Log" value="2" />
|
||||
<Box
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
||||
>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<Tab className={classes.root} label="Code" value="1" />
|
||||
<Tab className={classes.root} label="Log" value="2" />
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Tab className={classes.root} label="Webout" value="3" />
|
||||
</TabList>
|
||||
</Box>
|
||||
<TabPanel value="1">
|
||||
{/* <Toolbar /> */}
|
||||
<Paper
|
||||
sx={{
|
||||
height: '70vh',
|
||||
marginTop: '50px',
|
||||
padding: '10px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
<Editor
|
||||
height="95%"
|
||||
value={fileContent}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={(val) => {
|
||||
if (val) setFileContent(val)
|
||||
}}
|
||||
/>
|
||||
</Paper>
|
||||
<Stack
|
||||
spacing={3}
|
||||
direction="row"
|
||||
sx={{ justifyContent: 'center', marginTop: '20px' }}
|
||||
>
|
||||
<Button variant="contained" onClick={handleRunBtnClick}>
|
||||
Run SAS Code
|
||||
</Tooltip>
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
<TabPanel style={{ paddingBottom: 0 }} value="1">
|
||||
<div className={classes.subMenu}>
|
||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
||||
<img
|
||||
draggable="false"
|
||||
style={{ width: '25px' }}
|
||||
src="/running-sas.png"
|
||||
></img>
|
||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||
</Button>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
<TabPanel value="2">
|
||||
<div
|
||||
id="sas_log"
|
||||
style={{ marginTop: '50px' }}
|
||||
dangerouslySetInnerHTML={{ __html: log }}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* <Toolbar /> */}
|
||||
<Paper
|
||||
sx={{
|
||||
height: 'calc(100vh - 170px)',
|
||||
padding: '10px',
|
||||
overflow: 'auto',
|
||||
position: 'relative'
|
||||
}}
|
||||
elevation={3}
|
||||
>
|
||||
<Editor
|
||||
height="98%"
|
||||
value={fileContent}
|
||||
onMount={handleEditorDidMount}
|
||||
options={{ readOnly: ctrlPressed }}
|
||||
onChange={(val) => {
|
||||
if (val) setFileContent(val)
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value="3">
|
||||
<div
|
||||
style={{ marginTop: '50px' }}
|
||||
dangerouslySetInnerHTML={{ __html: webout }}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Box>
|
||||
</>
|
||||
<p
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: -10,
|
||||
textAlign: 'center',
|
||||
fontSize: '13px'
|
||||
}}
|
||||
>
|
||||
Press CTRL + ENTER to run SAS code
|
||||
</p>
|
||||
</Paper>
|
||||
</TabPanel>
|
||||
<TabPanel value="2">
|
||||
<div style={{ marginTop: '50px' }}>
|
||||
<h2>SAS Log</h2>
|
||||
<pre>{log}</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel value="3">
|
||||
<div style={{ marginTop: '50px' }}>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||