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

Compare commits

...

57 Commits

Author SHA1 Message Date
Saad Jutt
adc5aca0f0 chore(release): 0.0.42 2022-03-23 22:49:29 +05:00
Muhammad Saad
71c6be6b84 Merge pull request #104 from sasjs/webout-raw
fix: execute api, webout as raw
2022-03-23 22:47:07 +05:00
Saad Jutt
9c751877d1 fix: execute api, webout as raw 2022-03-23 22:41:02 +05:00
Saad Jutt
2204d54cd6 chore(release): 0.0.41 2022-03-23 21:32:06 +05:00
Saad Jutt
f4eb75ff34 fix(scroll): closes #100 2022-03-23 21:31:54 +05:00
Saad Jutt
a3cde343b7 chore(release): 0.0.40 2022-03-23 20:17:15 +05:00
Saad Jutt
7a70d40dbf fix: macros available for SAS 2022-03-23 20:15:18 +05:00
Saad Jutt
d27e070fc8 fix: moved macros from codebase to drive
This reverts commit d2956fc641.
2022-03-23 19:38:29 +05:00
Saad Jutt
27e260e6a4 fix(deploy): validating empty file or service in filetree 2022-03-23 19:38:20 +05:00
Saad Jutt
2796db8ead chore(release): 0.0.39 2022-03-23 18:07:12 +05:00
Muhammad Saad
84f7c2ab89 Merge pull request #103 from sasjs/executable-macros-fix
Executable macros fix
2022-03-23 18:07:01 +05:00
Saad Jutt
e68090181a fix: included sasjs core macros at compile time 2022-03-23 18:05:03 +05:00
Saad Jutt
d2956fc641 Revert "fix: moved macros from codebase to drive"
This reverts commit 9ac3191891.
2022-03-23 17:59:06 +05:00
Saad Jutt
a701bb25e7 Revert "fix: quick fix for executables"
This reverts commit 9e53470947.
2022-03-23 17:58:18 +05:00
Saad Jutt
5758bcd392 chore(release): 0.0.38 2022-03-23 17:16:01 +05:00
Saad Jutt
9e53470947 fix: quick fix for executables 2022-03-23 17:15:46 +05:00
Saad Jutt
81f6605249 chore(release): 0.0.37 2022-03-23 09:23:20 +05:00
Muhammad Saad
0b45402946 Merge pull request #102 from sasjs/issue-95
fix: moved macros from codebase to drive
2022-03-23 09:23:01 +05:00
Saad Jutt
9ac3191891 fix: moved macros from codebase to drive 2022-03-23 09:19:33 +05:00
Saad Jutt
cd00aa2af8 fix: appStream html view 2022-03-22 21:52:39 +05:00
Saad Jutt
0147bcb701 fix(webin): closes #99 2022-03-22 21:28:31 +05:00
Saad Jutt
bf53ad30f4 chore(release): 0.0.36 2022-03-22 04:07:27 +05:00
Muhammad Saad
a003b8836b Merge pull request #98 from sasjs/issue-91
feat: App Stream
2022-03-22 04:02:26 +05:00
Saad Jutt
df6003df94 fix(appstream): app logo + improvements 2022-03-22 03:55:51 +05:00
Saad Jutt
b1d0fdbb02 chore(release): 0.0.35 2022-03-21 18:24:35 +05:00
Muhammad Saad
2c34395110 Merge pull request #97 from sasjs/issue-96
feat(cors): whitelisting is configurable through .env variables
2022-03-21 18:21:39 +05:00
Saad Jutt
534e4e5bf3 chore: README.md updated 2022-03-21 18:17:26 +05:00
Saad Jutt
6146372eba chore: README.md updated 2022-03-21 18:05:40 +05:00
Saad Jutt
aaa469a142 chore: .env.example updated 2022-03-21 17:54:20 +05:00
Saad Jutt
4fd5bf948e fix(cors): removed trailing slashes of urls 2022-03-21 17:49:28 +05:00
Saad Jutt
99f91fbce2 feat(cors): whitelisting is configurable through .env variables 2022-03-21 17:36:42 +05:00
Saad Jutt
98a00ec7ac feat: App Stream, load on startup, new route added 2022-03-21 17:17:29 +05:00
munja
b0fb858c49 chore(release): 0.0.34 2022-03-18 23:37:37 +00:00
Allan Bowe
83959ef99e Update README.md 2022-03-18 23:36:15 +00:00
Allan Bowe
08087495d3 Merge pull request #94 from sasjs/add-license-1
Create LICENSE
2022-03-18 23:53:05 +02:00
Allan Bowe
3f68474839 Create LICENSE 2022-03-18 14:44:00 +00:00
Allan Bowe
f26886f84d Merge pull request #93 from sasjs/build-files
chore: copy other files as part of npm run build
2022-03-18 16:42:09 +02:00
Allan Bowe
ddd50eac8e Merge pull request #92 from sasjs/issue-90
fix(stp): write original file name in sas code for upload
2022-03-18 16:41:45 +02:00
Saad Jutt
bba3e8d272 chore: reduced stp sample data 2022-03-18 16:30:54 +05:00
Saad Jutt
30944bfa18 chore: copy other files as part of npm run build 2022-03-18 16:24:34 +05:00
Saad Jutt
8822de95df fix(stp): write original file name in sas code for upload 2022-03-18 06:57:52 +05:00
Allan Bowe
02a242fe4b Merge pull request #88 from sasjs/quick-fix
fix: web index js script included
2022-03-17 11:55:03 +02:00
Allan Bowe
1beac914db Merge pull request #89 from sasjs/remove-deleted-file
fix: remove deleted file from directory tree in sidebar of drive
2022-03-17 11:54:35 +02:00
Saad Jutt
a45b42107e chore: fixed specs 2022-03-17 07:22:53 +05:00
3d89b753f0 feat(web): directory tree in sidebar of drive should be expanded by default at root level 2022-03-16 19:13:50 +00:00
fb77d99177 fix(web-drive): upon delete remove entry of deleted file from directory tree in sidebar 2022-03-16 18:52:00 +00:00
fa627aabf9 chore(web-refactor): state uplifted for drive container in web component 2022-03-16 18:48:18 +00:00
Saad Jutt
fd2629862f fix: preferred to show param errors from query 2022-03-16 19:21:39 +05:00
Saad Jutt
75291f9397 fix: desktop mode web index.html js script included 2022-03-16 19:20:38 +05:00
Saad Jutt
99fb5f4b2b chore(release): 0.0.33 2022-03-16 06:44:35 +05:00
Muhammad Saad
5dc3deeb11 Merge pull request #86 from sasjs/issue-80
Issue 80
2022-03-16 06:24:41 +05:00
Saad Jutt
6b708fcad3 fix: added api button on web component 2022-03-16 06:22:26 +05:00
Saad Jutt
bc0ff84d8d chore: updated code as per sasjs/utils breaking change 2022-03-16 05:04:33 +05:00
Saad Jutt
1ff6965dd2 fix: adde validation + code improvement 2022-03-16 04:53:07 +05:00
Saad Jutt
d6fa877941 feat: serve deployed streaming apps 2022-03-15 03:54:19 +05:00
Saad Jutt
940f705f5d chore(release): 0.0.32 2022-03-14 06:26:38 +05:00
Saad Jutt
7a6e6c8bec feat(web): added delete option in Drive 2022-03-14 06:26:12 +05:00
44 changed files with 1268 additions and 390 deletions

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16.14.0

View File

@@ -2,6 +2,116 @@
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.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)
### Features
* **web:** added delete option in Drive ([7a6e6c8](https://github.com/sasjs/server/commit/7a6e6c8becab31410d0a36bcc22e13d5359a6cdf))
### [0.0.31](https://github.com/sasjs/server/compare/v0.0.30...v0.0.31) (2022-03-14)

21
LICENSE Normal file
View 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.

110
README.md
View File

@@ -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,58 @@ 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:
```
MODE=desktop # options: [desktop|server] default: `desktop`
CORS=disable # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
WHITELIST= # options: <http://localhost:3000 https://abc.com ...> space separated urls
PROTOCOL=http # options: [http|https] default: http
PORT=5000 # default: 5000
# 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
```
## 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 +88,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 +110,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`

View File

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

@@ -0,0 +1 @@
v16.14.0

408
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.36.2",
"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/**/*"
]
}
}

21
api/public/sasjs-logo.svg Normal file
View 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

View File

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

View File

@@ -17,6 +17,7 @@ const compiledSystemInit = async (systemInit: string) =>
programFolders: [],
macroFolders: [],
buildSourceFolder: '',
binaryFolders: [],
macroCorePath
}))

View File

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

View File

@@ -7,7 +7,9 @@ import cors from 'cors'
import {
connectDB,
copySASjsCore,
getWebBuildFolderPath,
loadAppStreamConfig,
sasJSCoreMacros,
setProcessVariables
} from './utils'
@@ -16,14 +18,17 @@ 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 }))
}
@@ -31,7 +36,6 @@ app.use(cookieParser())
app.use(morgan('tiny'))
app.use(express.json({ limit: '50mb' }))
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,18 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
}
export default setProcessVariables().then(async () => {
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)

View File

@@ -28,13 +28,15 @@ import { FileTree, isFileTree, 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
}
@@ -190,14 +192,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 }
})

View File

@@ -13,9 +13,9 @@ import {
extractHeaders,
generateFileUploadSasCode,
getTmpFilesFolderPath,
getTmpMacrosPath,
HTTPHeaders,
isDebugOn,
sasJSCoreMacros
isDebugOn
} from '../../utils'
export interface ExecutionVars {
@@ -106,7 +106,7 @@ export class ExecutionController {
`
program = `
options insert=(SASAUTOS="${sasJSCoreMacros}");
options insert=(SASAUTOS="${getTmpMacrosPath()}");
/* runtime vars */
${varStatments}
@@ -119,7 +119,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

View File

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

View File

@@ -1,11 +1,17 @@
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types'
import path from 'path'
import {
MemberType,
FolderMember,
ServiceMember,
FileTree,
FileMember
} from '../../types'
import { getTmpFilesFolderPath } from '../../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
import path from 'path'
// REFACTOR: export FileTreeCpntroller
export const createFileTree = async (
members: (FolderMember | ServiceMember)[],
members: (FolderMember | ServiceMember | FileMember)[],
parentFolders: string[] = []
) => {
const destinationPath = path.join(
@@ -13,25 +19,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()
}

View File

@@ -1,9 +1,8 @@
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
@@ -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.`)
)
}

View File

@@ -1,17 +1,37 @@
import express from 'express'
import { deleteFile } 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
} 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
@@ -26,7 +46,7 @@ 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 && errB) return res.status(400).send(errQ.details[0].message)
try {
await controller.getFile(req, query._filePath, body.filePath)
@@ -39,7 +59,7 @@ 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 && errB) return res.status(400).send(errQ.details[0].message)
try {
const response = await controller.deleteFile(query._filePath, body.filePath)
@@ -58,7 +78,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 +106,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.')

View File

@@ -74,14 +74,19 @@ describe('files', () => {
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 +145,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 +159,7 @@ describe('files', () => {
const testJobFolder = path.join(
getTmpFilesFolderPath(),
'public',
'jobs',
'extract'
)
@@ -254,22 +260,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 +293,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,9 +303,7 @@ 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({})
})
@@ -414,22 +418,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')
.patch(`/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({})
})
@@ -447,7 +451,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,9 +461,7 @@ 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({})
})

View File

@@ -0,0 +1,54 @@
import { AppStreamConfig } from '../../types'
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;
border-radius: 10px 10px 0 0;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
}
</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}" />
${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('')}
</div>
</body>
</html>`

View File

@@ -0,0 +1,76 @@
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}`
} else {
const alreadyDeployed = process.appStreamConfig[streamServiceName]
if (alreadyDeployed) {
if (alreadyDeployed.appLoc === appLoc) {
// redeploying to same streamServiceName
} else {
// trying to deploy to another existing streamServiceName
// assign new streamServiceName
streamServiceName = `${streamServiceName}-${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

View File

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

View File

@@ -0,0 +1,7 @@
export interface AppStreamConfig {
[key: string]: {
appLoc: string
streamWebFolder: string
streamLogo?: string
}
}

View File

@@ -1,16 +1,7 @@
export interface FileTree {
members: (FolderMember | ServiceMember)[]
}
export enum MemberType {
folder = 'folder',
service = 'service'
}
export interface FolderMember {
name: string
type: MemberType.folder
members: (FolderMember | ServiceMember)[]
service = 'service',
file = 'file',
folder = 'folder'
}
export interface ServiceMember {
@@ -19,16 +10,45 @@ export interface ServiceMember {
code: string
}
export interface FileMember {
name: string
type: MemberType.file
code: string
}
export interface FolderMember {
name: string
type: MemberType.folder
members: (FolderMember | ServiceMember | FileMember)[]
}
export interface FileTree {
members: (FolderMember | ServiceMember | FileMember)[]
}
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)
(member: ServiceMember | FileMember | FolderMember) =>
!isServiceMember(member, '-') &&
!isFileMember(member, '-') &&
!isFolderMember(member, '-')
).length === 0
const isFolderMember = (arg: any): arg is FolderMember =>
const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
typeof arg.code === 'string'
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.file &&
typeof arg.code === 'string'
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.folder &&
@@ -36,12 +56,7 @@ const isFolderMember = (arg: any): arg is FolderMember =>
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
!isServiceMember(member, pre + '-') &&
!isFileMember(member, pre + '-') &&
!isFolderMember(member, pre + '-')
).length === 0
const isServiceMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
arg.code &&
typeof arg.code === 'string'

View File

@@ -3,5 +3,6 @@ declare namespace NodeJS {
sasLoc: string
driveLoc: string
sessionController?: import('../controllers/internal').SessionController
appStreamConfig: import('./').AppStreamConfig
}
}

View File

@@ -1,4 +1,5 @@
// TODO: uppercase types
export * from './AppStreamConfig'
export * from './Execution'
export * from './FileTree'
export * from './InfoJWT'

View File

@@ -0,0 +1,87 @@
import { createFile, fileExists, readFile } from '@sasjs/utils'
import { publishAppStream } from '../routes/appStream'
import { AppStreamConfig } from '../types'
import { getTmpAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
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
}

View File

@@ -0,0 +1,32 @@
import path from 'path'
import {
asyncForEach,
createFile,
createFolder,
deleteFolder,
readFile
} from '@sasjs/utils'
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
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)
}

View File

@@ -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 = () =>

View File

@@ -1,12 +1,14 @@
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'

View File

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

View File

@@ -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,34 @@ 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 runSASValidation = (data: any): Joi.ValidationResult =>

20
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "server",
"version": "0.0.31",
"version": "0.0.42",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "server",
"version": "0.0.31",
"version": "0.0.42",
"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",
@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "server",
"version": "0.0.31",
"version": "0.0.42",
"description": "NodeJS wrapper for calling the SAS binary executable",
"repository": "https://github.com/sasjs/server",
"scripts": {

25
restClient/stp.rest Normal file
View 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
View File

@@ -0,0 +1 @@
v16.14.0

17
web/package-lock.json generated
View File

@@ -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",
@@ -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
},

View File

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

View File

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

View File

@@ -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)
@@ -23,7 +28,7 @@ const Main = (props: any) => {
if (props.selectedFilePath) {
setIsLoading(true)
axios
.get(`/SASjsApi/drive/file?filePath=${props.selectedFilePath}`)
.get(`/SASjsApi/drive/file?_filePath=${props.selectedFilePath}`)
.then((res: any) => {
setFileContent(res.data)
})
@@ -36,6 +41,26 @@ const Main = (props: any) => {
}
}, [props.selectedFilePath])
const handleDeleteBtnClick = () => {
setIsLoading(true)
const filePath = props.selectedFilePath
axios
.delete(`/SASjsApi/drive/file?_filePath=${filePath}`)
.then((res) => {
setFileContent('')
props.removeFileFromTree(filePath)
window.history.pushState('', '', `${baseUrl}/#/SASjsDrive`)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}
const handleEditSaveBtnClick = () => {
if (!editMode) {
setFileContentBeforeEdit(fileContent)
@@ -112,6 +137,13 @@ const Main = (props: any) => {
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button
variant="contained"
onClick={handleDeleteBtnClick}
disabled={isLoading || !props?.selectedFilePath}
>
Delete
</Button>
<Button
variant="contained"
onClick={handleEditSaveBtnClick}

View File

@@ -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,44 +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) {
window.history.pushState(
'',
'',
`${baseUrl}/#/SASjsDrive?filePath=${node.relativePath}`
)
setSelectedFilePath(node.relativePath)
}
}
const renderTree = (nodes: TreeNode) => (
<TreeItem
classes={{ root: classes.root }}
@@ -105,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>
)

View File

@@ -54,11 +54,11 @@ const Studio = () => {
let weboutString: string
try {
weboutString = res.data.webout
weboutString = res.data._webout
.split('>>weboutBEGIN<<')[1]
.split('>>weboutEND<<')[0]
} catch (_) {
weboutString = res?.data?.webout ?? ''
weboutString = res?.data?._webout ?? ''
}
let webout: string
@@ -70,6 +70,9 @@ const Studio = () => {
setWebout(`<pre><code>${webout}</code></pre>`)
setTab('2')
// Scroll to bottom of log
window.scrollTo(0, document.body.scrollHeight)
})
.catch((err) => console.log(err))
}