mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
505f2089c7 | ||
|
|
3344c400a8 | ||
| fa6248e3ef | |||
| 9fb5f1f8e7 | |||
|
|
92e0b8a088 | ||
|
|
b484306ed8 | ||
| 5e08aacc51 | |||
| a9e4eb685d | |||
| 31b09f27cc | |||
| 9f3ec92f8e | |||
| 6c9e449614 | |||
| 68e84b0994 | |||
| f0bb51a0d5 | |||
| b93a0da3a3 | |||
|
|
e5facbf54c | ||
|
|
cb2bebbe76 | ||
|
|
9e1e0ce8cc | ||
|
|
29928753b7 | ||
|
|
edd69ecaae | ||
|
|
74ba65f9f3 | ||
|
|
f257602834 | ||
|
|
61080d4694 | ||
|
|
82633adbc4 | ||
|
|
23db7e7b7d | ||
|
|
cbaa687c9b | ||
|
|
527f70e90d |
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@@ -113,3 +113,7 @@ cd ./api && npm i && npm run exe
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will install/build web app and install/create executables of sasjs server at root `./executables`
|
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)
|
||||||
|
|||||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -2,6 +2,42 @@
|
|||||||
|
|
||||||
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.
|
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.52](https://github.com/sasjs/server/compare/v0.0.51...v0.0.52) (2022-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add api for getting server info ([9fb5f1f](https://github.com/sasjs/server/commit/9fb5f1f8e7d4e2d767cc1ff7285c99514834cf32))
|
||||||
|
|
||||||
|
### [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)
|
### [0.0.48](https://github.com/sasjs/server/compare/v0.0.47...v0.0.48) (2022-04-02)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -52,6 +52,7 @@ Example contents of a `.env` file:
|
|||||||
MODE=
|
MODE=
|
||||||
|
|
||||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||||
|
# If enabled, be sure to also configure the WHITELIST of third party servers.
|
||||||
CORS=
|
CORS=
|
||||||
|
|
||||||
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||||
@@ -84,6 +85,15 @@ ACCESS_TOKEN_SECRET=<secret>
|
|||||||
REFRESH_TOKEN_SECRET=<secret>
|
REFRESH_TOKEN_SECRET=<secret>
|
||||||
AUTH_CODE_SECRET=<secret>
|
AUTH_CODE_SECRET=<secret>
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
|
# 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
|
## Persisting the Session
|
||||||
|
|||||||
BIN
api/public/plus.png
Normal file
BIN
api/public/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 899 B |
@@ -418,6 +418,25 @@ components:
|
|||||||
example: /Public/somefolder/some.file
|
example: /Public/somefolder/some.file
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
InfoResponse:
|
||||||
|
properties:
|
||||||
|
mode:
|
||||||
|
type: string
|
||||||
|
cors:
|
||||||
|
type: string
|
||||||
|
whiteList:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
protocol:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- mode
|
||||||
|
- cors
|
||||||
|
- whiteList
|
||||||
|
- protocol
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -1240,10 +1259,31 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
||||||
|
/SASjsApi/info:
|
||||||
|
get:
|
||||||
|
operationId: Info
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/InfoResponse'
|
||||||
|
examples:
|
||||||
|
'Example 1':
|
||||||
|
value: {mode: desktop, cors: enable, whiteList: ['http://example.com', 'http://example2.com'], protocol: http}
|
||||||
|
summary: 'Get server info (mode, cors, whiteList, protocol).'
|
||||||
|
tags:
|
||||||
|
- Info
|
||||||
|
security: []
|
||||||
|
parameters: []
|
||||||
servers:
|
servers:
|
||||||
-
|
-
|
||||||
url: /
|
url: /
|
||||||
tags:
|
tags:
|
||||||
|
-
|
||||||
|
name: Info
|
||||||
|
description: 'Get Server Info'
|
||||||
-
|
-
|
||||||
name: Session
|
name: Session
|
||||||
description: 'Get Session information'
|
description: 'Get Session information'
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from './group'
|
|||||||
export * from './session'
|
export * from './session'
|
||||||
export * from './stp'
|
export * from './stp'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
|
export * from './info'
|
||||||
|
|||||||
37
api/src/controllers/info.ts
Normal file
37
api/src/controllers/info.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { Request, Security, Route, Tags, Example, Get } from 'tsoa'
|
||||||
|
|
||||||
|
export interface InfoResponse {
|
||||||
|
mode: string
|
||||||
|
cors: string
|
||||||
|
whiteList: string[]
|
||||||
|
protocol: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Route('SASjsApi/info')
|
||||||
|
@Tags('Info')
|
||||||
|
export class InfoController {
|
||||||
|
/**
|
||||||
|
* @summary Get server info (mode, cors, whiteList, protocol).
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Example<InfoResponse>({
|
||||||
|
mode: 'desktop',
|
||||||
|
cors: 'enable',
|
||||||
|
whiteList: ['http://example.com', 'http://example2.com'],
|
||||||
|
protocol: 'http'
|
||||||
|
})
|
||||||
|
@Get('/')
|
||||||
|
public info(): InfoResponse {
|
||||||
|
const response = {
|
||||||
|
mode: process.env.MODE ?? 'desktop',
|
||||||
|
cors:
|
||||||
|
process.env.CORS ?? process.env.MODE === 'server'
|
||||||
|
? 'disable'
|
||||||
|
: 'enable',
|
||||||
|
whiteList: process.env.WHITELIST?.split(' ') ?? [],
|
||||||
|
protocol: process.env.PROTOCOL ?? 'http'
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,7 +157,9 @@ ${program}`
|
|||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
const fileResponse: boolean =
|
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))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
createFile,
|
createFile,
|
||||||
fileExists,
|
fileExists,
|
||||||
generateTimestamp,
|
generateTimestamp,
|
||||||
readFile,
|
readFile
|
||||||
moveFile
|
|
||||||
} from '@sasjs/utils'
|
} from '@sasjs/utils'
|
||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
@@ -41,6 +40,7 @@ export class SessionController {
|
|||||||
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
const sessionFolder = path.join(getTmpSessionsFolderPath(), sessionId)
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
// death time of session is 15 mins from creation
|
||||||
const deathTimeStamp = (
|
const deathTimeStamp = (
|
||||||
parseInt(creationTimeStamp) +
|
parseInt(creationTimeStamp) +
|
||||||
15 * 60 * 1000 -
|
15 * 60 * 1000 -
|
||||||
@@ -140,7 +140,9 @@ ${autoExecContent}`
|
|||||||
private scheduleSessionDestroy(session: Session) {
|
private scheduleSessionDestroy(session: Session) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
if (session.inUse) {
|
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)
|
this.scheduleSessionDestroy(session)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
verifyAdmin
|
verifyAdmin
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
|
|
||||||
|
import infoRouter from './info'
|
||||||
import driveRouter from './drive'
|
import driveRouter from './drive'
|
||||||
import stpRouter from './stp'
|
import stpRouter from './stp'
|
||||||
import codeRouter from './code'
|
import codeRouter from './code'
|
||||||
@@ -20,6 +21,7 @@ import sessionRouter from './session'
|
|||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.use('/info', infoRouter)
|
||||||
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
router.use('/session', desktopUsername, authenticateAccessToken, sessionRouter)
|
||||||
router.use('/auth', desktopRestrict, authRouter)
|
router.use('/auth', desktopRestrict, authRouter)
|
||||||
router.use(
|
router.use(
|
||||||
|
|||||||
16
api/src/routes/api/info.ts
Normal file
16
api/src/routes/api/info.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { InfoController } from '../../controllers'
|
||||||
|
|
||||||
|
const infoRouter = express.Router()
|
||||||
|
|
||||||
|
infoRouter.get('/', async (req, res) => {
|
||||||
|
const controller = new InfoController()
|
||||||
|
try {
|
||||||
|
const response = controller.info()
|
||||||
|
res.send(response)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default infoRouter
|
||||||
14
api/src/routes/api/spec/info.spec.ts
Normal file
14
api/src/routes/api/spec/info.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Express } from 'express'
|
||||||
|
import request from 'supertest'
|
||||||
|
import appPromise from '../../../app'
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
|
||||||
|
describe('Info', () => {
|
||||||
|
it('should should return configured information of the server instance', async () => {
|
||||||
|
await appPromise.then((_app) => {
|
||||||
|
app = _app
|
||||||
|
})
|
||||||
|
request(app).get('/SASjsApi/info').expect(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,27 +1,6 @@
|
|||||||
import { AppStreamConfig } from '../../types'
|
import { AppStreamConfig } from '../../types'
|
||||||
|
import { script } from './script'
|
||||||
const style = `<style>
|
import { style } from './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>`
|
|
||||||
|
|
||||||
const defaultAppLogo = '/sasjs-logo.svg'
|
const defaultAppLogo = '/sasjs-logo.svg'
|
||||||
|
|
||||||
@@ -52,6 +31,14 @@ export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
|
|||||||
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
|
||||||
)
|
)
|
||||||
.join('')}
|
.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>
|
</div>
|
||||||
|
${script}
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|||||||
58
api/src/routes/appStream/script.ts
Normal file
58
api/src/routes/appStream/script.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export const script = `<script>
|
||||||
|
const inputElement = document.getElementById('fileId')
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('uploadButton')
|
||||||
|
.addEventListener('click', function () {
|
||||||
|
inputElement.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
inputElement.addEventListener(
|
||||||
|
'change',
|
||||||
|
function () {
|
||||||
|
const fileList = this.files /* now you can work with the file list */
|
||||||
|
|
||||||
|
updateFileUploadMessage('Requesting ...')
|
||||||
|
|
||||||
|
const file = fileList[0]
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('file', file)
|
||||||
|
fetch('/SASjsApi/drive/deploy/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
const { status, ok } = res
|
||||||
|
if (status === 200 && ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
return (
|
||||||
|
data.message +
|
||||||
|
'\\nstreamServiceName: ' +
|
||||||
|
data.streamServiceName +
|
||||||
|
'\\nrefreshing page once alert box closes.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw await res.text()
|
||||||
|
})
|
||||||
|
.then((message) => {
|
||||||
|
alert(message)
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert(error)
|
||||||
|
resetFileUpload()
|
||||||
|
updateFileUploadMessage('Upload New App')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
function updateFileUploadMessage(message) {
|
||||||
|
document.getElementById('uploadMessage').innerHTML = message
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFileUpload() {
|
||||||
|
inputElement.value = null
|
||||||
|
}
|
||||||
|
</script>`
|
||||||
22
api/src/routes/appStream/style.ts
Normal file
22
api/src/routes/appStream/style.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const style = `<style>
|
||||||
|
* {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.app-container .app {
|
||||||
|
width: 150px;
|
||||||
|
margin: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.app-container .app img{
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
</style>`
|
||||||
@@ -14,7 +14,6 @@ export * from './removeTokensInDB'
|
|||||||
export * from './saveTokensInDB'
|
export * from './saveTokensInDB'
|
||||||
export * from './setProcessVariables'
|
export * from './setProcessVariables'
|
||||||
export * from './setupFolders'
|
export * from './setupFolders'
|
||||||
export * from './sleep'
|
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyTokenInDB'
|
export * from './verifyTokenInDB'
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export const sleep = async (delay: number) => {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "Info",
|
||||||
|
"description": "Get Server Info"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Session",
|
"name": "Session",
|
||||||
"description": "Get Session information"
|
"description": "Get Session information"
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.48",
|
"version": "0.0.52",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.48",
|
"version": "0.0.52",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"standard-version": "^9.3.2"
|
"standard-version": "^9.3.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.48",
|
"version": "0.0.52",
|
||||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||||
"repository": "https://github.com/sasjs/server",
|
"repository": "https://github.com/sasjs/server",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
BIN
web/public/running-sas.png
Normal file
BIN
web/public/running-sas.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -28,10 +28,10 @@ const Header = (props: any) => {
|
|||||||
>
|
>
|
||||||
<Toolbar variant="dense">
|
<Toolbar variant="dense">
|
||||||
<img
|
<img
|
||||||
src="logo-white.png"
|
src="logo.png"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
style={{
|
style={{
|
||||||
width: '50px',
|
width: '35px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
marginRight: '25px'
|
marginRight: '25px'
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import Box from '@mui/material/Box'
|
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 { makeStyles } from '@mui/styles'
|
||||||
import Editor, { OnMount } from '@monaco-editor/react'
|
import Editor, { OnMount } from '@monaco-editor/react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
@@ -15,6 +15,17 @@ const useStyles = makeStyles(() => ({
|
|||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
color: 'black'
|
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 location = useLocation()
|
||||||
const [fileContent, setFileContent] = useState('')
|
const [fileContent, setFileContent] = useState('')
|
||||||
const [log, setLog] = useState('')
|
const [log, setLog] = useState('')
|
||||||
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
const [webout, setWebout] = useState('')
|
const [webout, setWebout] = useState('')
|
||||||
const [tab, setTab] = React.useState('1')
|
const [tab, setTab] = React.useState('1')
|
||||||
|
|
||||||
const handleTabChange = (_e: any, newValue: string) => {
|
const handleTabChange = (_e: any, newValue: string) => {
|
||||||
setTab(newValue)
|
setTab(newValue)
|
||||||
}
|
}
|
||||||
@@ -61,6 +74,21 @@ const Studio = () => {
|
|||||||
.catch((err) => console.log(err))
|
.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(() => {
|
useEffect(() => {
|
||||||
const content = localStorage.getItem('fileContent') ?? ''
|
const content = localStorage.getItem('fileContent') ?? ''
|
||||||
setFileContent(content)
|
setFileContent(content)
|
||||||
@@ -86,70 +114,87 @@ const Studio = () => {
|
|||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box
|
||||||
<br />
|
onKeyUp={handleKeyUp}
|
||||||
<br />
|
onKeyDown={handleKeyDown}
|
||||||
<br />
|
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
||||||
<Box sx={{ width: '100%', typography: 'body1' }}>
|
>
|
||||||
<TabContext value={tab}>
|
<TabContext value={tab}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
borderBottom: 1,
|
borderBottom: 1,
|
||||||
borderColor: 'divider'
|
borderColor: 'divider'
|
||||||
}}
|
}}
|
||||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
||||||
>
|
>
|
||||||
<TabList onChange={handleTabChange} centered>
|
<TabList onChange={handleTabChange} centered>
|
||||||
<Tab className={classes.root} label="Code" value="1" />
|
<Tab className={classes.root} label="Code" value="1" />
|
||||||
<Tab className={classes.root} label="Log" value="2" />
|
<Tab className={classes.root} label="Log" value="2" />
|
||||||
|
<Tooltip title="Displays content from the _webout fileref">
|
||||||
<Tab className={classes.root} label="Webout" value="3" />
|
<Tab className={classes.root} label="Webout" value="3" />
|
||||||
</TabList>
|
</Tooltip>
|
||||||
</Box>
|
</TabList>
|
||||||
<TabPanel value="1">
|
</Box>
|
||||||
{/* <Toolbar /> */}
|
|
||||||
<Paper
|
<TabPanel style={{ paddingBottom: 0 }} value="1">
|
||||||
sx={{
|
<div className={classes.subMenu}>
|
||||||
height: '70vh',
|
<Tooltip title="CTRL+ENTER will also run SAS code">
|
||||||
marginTop: '50px',
|
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
||||||
padding: '10px',
|
<img
|
||||||
overflow: 'auto',
|
draggable="false"
|
||||||
position: 'relative'
|
style={{ width: '25px' }}
|
||||||
}}
|
src="/running-sas.png"
|
||||||
elevation={3}
|
></img>
|
||||||
>
|
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||||
<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
|
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Tooltip>
|
||||||
</TabPanel>
|
</div>
|
||||||
<TabPanel value="2">
|
{/* <Toolbar /> */}
|
||||||
<div style={{ marginTop: '50px' }}>
|
<Paper
|
||||||
<h2>SAS Log</h2>
|
sx={{
|
||||||
<pre>{log}</pre>
|
height: 'calc(100vh - 170px)',
|
||||||
</div>
|
padding: '10px',
|
||||||
</TabPanel>
|
overflow: 'auto',
|
||||||
<TabPanel value="3">
|
position: 'relative'
|
||||||
<div style={{ marginTop: '50px' }}>
|
}}
|
||||||
<pre>{webout}</pre>
|
elevation={3}
|
||||||
</div>
|
>
|
||||||
</TabPanel>
|
<Editor
|
||||||
</TabContext>
|
height="98%"
|
||||||
</Box>
|
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user