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

Compare commits

..

31 Commits

Author SHA1 Message Date
munja
92e0b8a088 chore(release): 0.0.51 2022-04-15 14:30:43 +01:00
Allan Bowe
b484306ed8 Merge pull request #126 from sasjs/issue-119
running code with CTRL+ENTER
2022-04-15 16:29:23 +03:00
5e08aacc51 chore: css fix 2022-04-15 14:53:36 +02:00
a9e4eb685d chore: style fix 2022-04-15 14:26:45 +02:00
31b09f27cc style: lint 2022-04-15 14:23:36 +02:00
9f3ec92f8e chore: run button style fix 2022-04-15 14:23:15 +02:00
6c9e449614 style: lint 2022-04-14 19:56:22 +02:00
68e84b0994 feat: run button running man, sub menu added 2022-04-14 19:38:44 +02:00
f0bb51a0d5 chore: placement of ctrl enter label 2022-04-13 22:12:40 +02:00
b93a0da3a3 feat: running code with CTRL+ENTER 2022-04-13 15:27:41 +02:00
Allan Bowe
e5facbf54c Update README.md 2022-04-13 12:24:42 +01:00
Allan Bowe
cb2bebbe76 Update README.md 2022-04-12 12:47:55 +01:00
Allan Bowe
9e1e0ce8cc chore(release): 0.0.50 2022-04-07 15:25:04 +00:00
Allan Bowe
29928753b7 Update CONTRIBUTING.md 2022-04-07 16:24:36 +01:00
Allan Bowe
edd69ecaae Merge pull request #122 from sasjs/issue121
Fixed couple of bugs + feature implemented
2022-04-07 18:23:34 +03:00
Saad Jutt
74ba65f9f3 feat(appstream): Upload an app from appStream page 2022-04-07 20:18:36 +05:00
Saad Jutt
f257602834 fix: web component UI fix for studio scrolling 2022-04-07 19:10:45 +05:00
Saad Jutt
61080d4694 fix: web component added tooltip for webout in studio 2022-04-07 18:59:31 +05:00
Saad Jutt
82633adbc4 chore: removed unused util 2022-04-07 18:48:31 +05:00
Saad Jutt
23db7e7b7d fix: session death time has to be a valid string number 2022-04-07 18:48:22 +05:00
Saad Jutt
cbaa687c9b chore(release): 0.0.49 2022-04-02 07:09:39 +05:00
Saad Jutt
527f70e90d fix(stp): read file in non-binary mode if debug one 2022-04-02 07:09:27 +05:00
Saad Jutt
122faad55f chore(release): 0.0.48 2022-04-02 07:06:18 +05:00
Saad Jutt
3ff6f5e865 fix(stp): return log+webout for debug on 2022-04-02 07:06:09 +05:00
Muhammad Saad
7d5128c0d6 Merge pull request #115 from sasjs/issue109
feat(deploy): new route added for deploy with build.json
2022-04-02 06:45:28 +05:00
Saad Jutt
e1ebbfd087 chore: increased file upload size to 100mb 2022-04-02 06:04:34 +05:00
Saad Jutt
e430bdb0d4 test(upload): spec updated for file upload exceeding limit 2022-04-02 05:51:24 +05:00
Saad Jutt
9d9769eef3 chore: increased file upload size to 100mb 2022-04-02 05:36:53 +05:00
Saad Jutt
9d167abe2a fix: remove uploaded build.json from temp folder in all cases 2022-04-02 05:29:34 +05:00
Saad Jutt
18d0604bdd feat(deploy): new route added for deploy with build.json 2022-04-02 05:23:25 +05:00
Saad Jutt
7b7bc6b778 chore: fix vulnerabilities 2022-03-31 01:54:40 +05:00
31 changed files with 427 additions and 203 deletions

View File

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

View File

@@ -2,6 +2,48 @@
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)

View File

@@ -52,6 +52,7 @@ Example contents of a `.env` file:
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
@@ -84,6 +85,15 @@ 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

14
api/package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.2",
"dependencies": {
"@sasjs/core": "4.9.0",
"@sasjs/utils": "2.36.2",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@@ -1384,9 +1384,9 @@
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
},
"node_modules/@sasjs/utils": {
"version": "2.36.2",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"hasInstallScript": true,
"dependencies": {
"@types/fs-extra": "9.0.13",
@@ -11132,9 +11132,9 @@
"integrity": "sha512-zc1Ey0ylHt/eRZAfK0mVG3EqNyq//wLxbiguiK0R6FhVqwYFEkprs3IiLGZ5M9ttKs2rHRIjOe/ckklHm+6HNQ=="
},
"@sasjs/utils": {
"version": "2.36.2",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.2.tgz",
"integrity": "sha512-r0O9vkNIK5+2peBiGbcKc3Ei62eAMDt+1SQl17U9Vv26LYqezxQBwIYYMUjnkZE8Q7XlTI/FUS+SIHTCZMr4Jg==",
"version": "2.42.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.42.1.tgz",
"integrity": "sha512-DzHNYjeoj2eUkwV7Sa4eHCKRoTrYaQ6eyv6c1U5qOYXwVdZpMoYA3HFsHj55UcMOn2U3CXI5nrR7PZlUmVwVbQ==",
"requires": {
"@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13",

View File

@@ -47,7 +47,7 @@
"author": "4GL Ltd",
"dependencies": {
"@sasjs/core": "4.9.0",
"@sasjs/utils": "2.36.2",
"@sasjs/utils": "2.42.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",

BIN
api/public/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 B

View File

@@ -606,6 +606,56 @@ 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

View File

@@ -1,5 +1,6 @@
import path from 'path'
import {
CompileTree,
createFile,
loadDependenciesFile,
readFile,
@@ -18,7 +19,8 @@ const compiledSystemInit = async (systemInit: string) =>
macroFolders: [],
buildSourceFolder: '',
binaryFolders: [],
macroCorePath
macroCorePath,
compileTree: new CompileTree('') // dummy compileTree
}))
const createSysInitFile = async () => {

View File

@@ -14,7 +14,8 @@ import {
Patch,
UploadedFile,
FormField,
Delete
Delete,
Hidden
} from 'tsoa'
import {
fileExists,
@@ -22,14 +23,15 @@ import {
createFolder,
deleteFile as deleteFileOnSystem,
folderExists,
listFilesAndSubFoldersInFolder,
listFilesInFolder,
listSubFoldersInFolder,
isFolder
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 {
@@ -93,6 +95,21 @@ export class DriveController {
return deploy(body)
}
/**
* @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

View File

@@ -157,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
@@ -178,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
}
}

View File

@@ -12,8 +12,7 @@ import {
createFile,
fileExists,
generateTimestamp,
readFile,
moveFile
readFile
} from '@sasjs/utils'
const execFilePromise = promisify(execFile)
@@ -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 -
@@ -140,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 {

View File

@@ -1,13 +1,15 @@
import path from 'path'
import { getTmpFilesFolderPath } from '../../utils/file'
import {
MemberType,
createFolder,
createFile,
asyncForEach,
FolderMember,
ServiceMember,
FileTree,
FileMember
} from '../../types'
import { getTmpFilesFolderPath } from '../../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
FileMember,
MemberType,
FileTree
} from '@sasjs/utils'
// REFACTOR: export FileTreeCpntroller
export const createFileTree = async (

View File

@@ -4,7 +4,7 @@ import multer, { FileFilterCallback, Options } from 'multer'
import { blockFileRegex, getTmpUploadsPath } from '../utils'
const fieldNameSize = 300
const fileSize = 10485760 // 10 MB
const fileSize = 104857600 // 100 MB
const storage = multer.diskStorage({
destination: getTmpUploadsPath(),

View File

@@ -1,5 +1,5 @@
import express from 'express'
import { deleteFile } from '@sasjs/utils'
import { deleteFile, readFile } from '@sasjs/utils'
import { publishAppStream } from '../appStream'
@@ -43,6 +43,54 @@ 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)

View File

@@ -12,7 +12,9 @@ import {
generateTimestamp,
copy,
createFolder,
createFile
createFile,
ServiceMember,
FolderMember
} from '@sasjs/utils'
import * as fileUtilModules from '../../../utils/file'
@@ -28,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
@@ -424,7 +425,7 @@ describe('drive', () => {
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')
@@ -434,7 +435,7 @@ describe('drive', () => {
.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({})
})
@@ -582,7 +583,7 @@ describe('drive', () => {
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')
@@ -592,7 +593,7 @@ describe('drive', () => {
.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({})
})

View File

@@ -1,27 +1,6 @@
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;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
border-radius: 10px;
}
</style>`
import { script } from './script'
import { style } from './style'
const defaultAppLogo = '/sasjs-logo.svg'
@@ -52,6 +31,14 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
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>`

View File

@@ -52,7 +52,7 @@ export const publishAppStream = async (
addEntryToFile
)
const sasJsPort = process.env.PORT ?? 5000
const sasJsPort = process.env.PORT || 5000
console.log(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`

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

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

View File

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

View File

@@ -1,62 +0,0 @@
export enum MemberType {
service = 'service',
file = 'file',
folder = 'folder'
}
export interface ServiceMember {
name: string
type: MemberType.service
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: ServiceMember | FileMember | FolderMember) =>
!isServiceMember(member, '-') &&
!isFileMember(member, '-') &&
!isFolderMember(member, '-')
).length === 0
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 &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isServiceMember(member, pre + '-') &&
!isFileMember(member, pre + '-') &&
!isFolderMember(member, pre + '-')
).length === 0

View File

@@ -1,7 +1,6 @@
// TODO: uppercase types
export * from './AppStreamConfig'
export * from './Execution'
export * from './FileTree'
export * from './InfoJWT'
export * from './PreProgramVars'
export * from './Request'

View File

@@ -14,7 +14,6 @@ export * from './removeTokensInDB'
export * from './saveTokensInDB'
export * from './setProcessVariables'
export * from './setupFolders'
export * from './sleep'
export * from './upload'
export * from './validation'
export * from './verifyTokenInDB'

View File

@@ -1,3 +0,0 @@
export const sleep = async (delay: number) => {
await new Promise((resolve) => setTimeout(resolve, delay))
}

16
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "server",
"version": "0.0.47",
"version": "0.0.51",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "server",
"version": "0.0.47",
"version": "0.0.51",
"devDependencies": {
"prettier": "^2.3.1",
"standard-version": "^9.3.2"
@@ -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": {
@@ -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": {

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
web/public/running-sas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -28,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'
}}

View File

@@ -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)
}
@@ -61,6 +74,21 @@ const Studio = () => {
.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)
@@ -86,70 +114,87 @@ const Studio = () => {
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 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>
</>
</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)
}}
/>
<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>
)
}