mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c874c2c39 | ||
|
|
d819d79bc9 | ||
| c51b50428f | |||
|
|
e10a0554f0 | ||
|
|
337e2eb2a0 | ||
| 66f8e7840b | |||
| 1c9d167f86 | |||
|
|
7e684b54a6 | ||
|
|
aafda2922b | ||
| 418bf41e38 | |||
| 81f0b03b09 | |||
| fe5ae44aab | |||
| 36be3a7d5e | |||
| 6434123401 | |||
|
|
0a6b972c65 | ||
|
|
be11707042 | ||
| 2412622367 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,8 +5,6 @@ node_modules/
|
|||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
sasjs_root/
|
sasjs_root/
|
||||||
api/mocks/custom/*
|
|
||||||
!api/mocks/custom/.keep
|
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
|||||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,3 +1,32 @@
|
|||||||
|
## [0.25.1](https://github.com/sasjs/server/compare/v0.25.0...v0.25.1) (2022-11-07)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **web:** use mui treeView instead of custom implementation ([c51b504](https://github.com/sasjs/server/commit/c51b50428f32608bc46438e9d7964429b2d595da))
|
||||||
|
|
||||||
|
# [0.25.0](https://github.com/sasjs/server/compare/v0.24.0...v0.25.0) (2022-11-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Enable DRIVE_LOCATION setting for deploying multiple instances of SASjs Server ([1c9d167](https://github.com/sasjs/server/commit/1c9d167f86bbbb108b96e9bc30efaf8de65d82ff))
|
||||||
|
|
||||||
|
# [0.24.0](https://github.com/sasjs/server/compare/v0.23.4...v0.24.0) (2022-10-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* cli mock testing ([6434123](https://github.com/sasjs/server/commit/643412340162e854f31fba2f162d83b7ab1751d8))
|
||||||
|
* mocking sas9 responses with JS STP ([36be3a7](https://github.com/sasjs/server/commit/36be3a7d5e7df79f9a1f3f00c3661b925f462383))
|
||||||
|
|
||||||
|
## [0.23.4](https://github.com/sasjs/server/compare/v0.23.3...v0.23.4) (2022-10-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add action to editor ref for running code ([2412622](https://github.com/sasjs/server/commit/2412622367eb46c40f388e988ae4606a7ec239b2))
|
||||||
|
|
||||||
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
|
## [0.23.3](https://github.com/sasjs/server/compare/v0.23.2...v0.23.3) (2022-10-09)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ R_PATH=/usr/bin/Rscript
|
|||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
|
||||||
|
# This location is for files, sasjs packages and appStreamConfig.json
|
||||||
|
DRIVE_LOCATION=./sasjs_root/drive
|
||||||
|
|
||||||
|
|
||||||
# options: [http|https] default: http
|
# options: [http|https] default: http
|
||||||
PROTOCOL=
|
PROTOCOL=
|
||||||
|
|
||||||
@@ -103,6 +107,11 @@ PORT=
|
|||||||
# If not present, mocking function is disabled
|
# If not present, mocking function is disabled
|
||||||
MOCK_SERVERTYPE=
|
MOCK_SERVERTYPE=
|
||||||
|
|
||||||
|
# default: /api/mocks
|
||||||
|
# Path to mocking folder, for generic responses, it's sub directories should be: sas9, viya, sasjs
|
||||||
|
# Server will automatically use subdirectory accordingly
|
||||||
|
STATIC_MOCK_LOCATION=
|
||||||
|
|
||||||
#
|
#
|
||||||
## Additional SAS Options
|
## Additional SAS Options
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ PYTHON_PATH=/usr/bin/python
|
|||||||
R_PATH=/usr/bin/Rscript
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
DRIVE_LOCATION=./sasjs_root/drive
|
||||||
|
|
||||||
LOG_FORMAT_MORGAN=common
|
LOG_FORMAT_MORGAN=common
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
|
||||||
<!--form container-->
|
<!--form container-->
|
||||||
<input type="hidden" name="lt" value="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" aria-hidden="true" />
|
<input type="hidden" name="lt" value="validtoken" aria-hidden="true" />
|
||||||
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
||||||
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ReturnCode,
|
ReturnCode,
|
||||||
setProcessVariables,
|
setProcessVariables,
|
||||||
setupFolders,
|
setupFolders,
|
||||||
|
setupUserAutoExec,
|
||||||
verifyEnvVariables
|
verifyEnvVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
import {
|
||||||
@@ -62,8 +63,12 @@ export default setProcessVariables().then(async () => {
|
|||||||
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||||
app.use(express.urlencoded({ extended: true }))
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
await setupFolders()
|
await setupUserAutoExec()
|
||||||
await copySASjsCore()
|
|
||||||
|
if (process.driveLoc === path.join(process.sasjsRoot, 'drive')) {
|
||||||
|
await setupFolders()
|
||||||
|
await copySASjsCore()
|
||||||
|
}
|
||||||
|
|
||||||
// loading these modules after setting up variables due to
|
// loading these modules after setting up variables due to
|
||||||
// multer's usage of process var process.driveLoc
|
// multer's usage of process var process.driveLoc
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface ExecuteFileParams {
|
|||||||
returnJson?: boolean
|
returnJson?: boolean
|
||||||
session?: Session
|
session?: Session
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
|
forceStringResult?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||||
@@ -42,7 +43,8 @@ export class ExecutionController {
|
|||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
returnJson,
|
||||||
session,
|
session,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
}: ExecuteFileParams) {
|
}: ExecuteFileParams) {
|
||||||
const program = await readFile(programPath)
|
const program = await readFile(programPath)
|
||||||
|
|
||||||
@@ -53,7 +55,8 @@ export class ExecutionController {
|
|||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
returnJson,
|
||||||
session,
|
session,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +66,8 @@ export class ExecutionController {
|
|||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime,
|
||||||
|
forceStringResult
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController = getSessionController(runTime)
|
||||||
|
|
||||||
@@ -104,7 +108,7 @@ export class ExecutionController {
|
|||||||
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse && !forceStringResult
|
||||||
? await readFileBinary(weboutPath)
|
? await readFileBinary(weboutPath)
|
||||||
: await readFile(weboutPath)
|
: await readFile(weboutPath)
|
||||||
: ''
|
: ''
|
||||||
|
|||||||
@@ -110,17 +110,13 @@ export const processProgram = async (
|
|||||||
|
|
||||||
// create a stream that will write to console outputs to log file
|
// create a stream that will write to console outputs to log file
|
||||||
const writeStream = fs.createWriteStream(logPath)
|
const writeStream = fs.createWriteStream(logPath)
|
||||||
|
|
||||||
// waiting for the open event so that we can have underlying file descriptor
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
await once(writeStream, 'open')
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
execFileSync(executablePath, [codePath], {
|
execFileSync(executablePath, [codePath], {
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
stdio: ['ignore', writeStream, writeStream]
|
||||||
})
|
})
|
||||||
|
|
||||||
// copy the code file to log and end write stream
|
// copy the code file to log and end write stream
|
||||||
writeStream.end(program)
|
writeStream.end(program)
|
||||||
|
|
||||||
session.completed = true
|
session.completed = true
|
||||||
console.log('session completed', session)
|
console.log('session completed', session)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ import { readFile } from '@sasjs/utils'
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { Request, Post, Get } from 'tsoa'
|
import { Request, Post, Get } from 'tsoa'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { ExecutionController } from './internal'
|
||||||
|
import {
|
||||||
|
getPreProgramVariables,
|
||||||
|
getRunTimeAndFilePath,
|
||||||
|
makeFilesNamesMap
|
||||||
|
} from '../utils'
|
||||||
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
export interface Sas9Response {
|
export interface Sas9Response {
|
||||||
content: string
|
content: string
|
||||||
@@ -16,9 +26,17 @@ export interface MockFileRead {
|
|||||||
|
|
||||||
export class MockSas9Controller {
|
export class MockSas9Controller {
|
||||||
private loggedIn: string | undefined
|
private loggedIn: string | undefined
|
||||||
|
private mocksPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||||
|
|
||||||
@Get('/SASStoredProcess')
|
@Get('/SASStoredProcess')
|
||||||
public async sasStoredProcess(): Promise<Sas9Response> {
|
public async sasStoredProcess(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
const username = req.query._username?.toString() || undefined
|
||||||
|
const password = req.query._password?.toString() || undefined
|
||||||
|
|
||||||
|
if (username && password) this.loggedIn = req.body.username
|
||||||
|
|
||||||
if (!this.loggedIn) {
|
if (!this.loggedIn) {
|
||||||
return {
|
return {
|
||||||
content: '',
|
content: '',
|
||||||
@@ -26,17 +44,87 @@ export class MockSas9Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let program = req.query._program?.toString() || undefined
|
||||||
|
const filePath: string[] = program
|
||||||
|
? program.replace('/', '').split('/')
|
||||||
|
: ['generic', 'sas-stored-process']
|
||||||
|
|
||||||
|
if (program) {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
this.mocksPath,
|
||||||
|
'sas9',
|
||||||
|
...filePath
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
'sas-stored-process'
|
...filePath
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess/do')
|
||||||
|
public async sasStoredProcessDoGet(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
const username = req.query._username?.toString() || undefined
|
||||||
|
const password = req.query._password?.toString() || undefined
|
||||||
|
|
||||||
|
if (username && password) this.loggedIn = username
|
||||||
|
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const program = req.query._program ?? req.body?._program
|
||||||
|
const filePath: string[] = ['generic', 'sas-stored-process']
|
||||||
|
|
||||||
|
if (program) {
|
||||||
|
const vars = { ...req.query, ...req.body, _requestMethod: req.method }
|
||||||
|
const otherArgs = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { codePath, runTime } = await getRunTimeAndFilePath(
|
||||||
|
program + '.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await new ExecutionController().executeFile({
|
||||||
|
programPath: codePath,
|
||||||
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
|
vars: vars,
|
||||||
|
otherArgs: otherArgs,
|
||||||
|
runTime,
|
||||||
|
forceStringResult: true
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: result.result as string
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('err', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: 'No webout returned.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'sas9',
|
||||||
|
...filePath
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/SASStoredProcess/do/')
|
@Post('/SASStoredProcess/do/')
|
||||||
public async sasStoredProcessDo(
|
public async sasStoredProcessDoPost(
|
||||||
@Request() req: express.Request
|
@Request() req: express.Request
|
||||||
): Promise<Sas9Response> {
|
): Promise<Sas9Response> {
|
||||||
if (!this.loggedIn) {
|
if (!this.loggedIn) {
|
||||||
@@ -53,23 +141,38 @@ export class MockSas9Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let program = req.query._program?.toString() || ''
|
const program = req.query._program ?? req.body?._program
|
||||||
program = program.replace('/', '')
|
const vars = {
|
||||||
|
...req.query,
|
||||||
|
...req.body,
|
||||||
|
_requestMethod: req.method,
|
||||||
|
_driveLoc: process.driveLoc
|
||||||
|
}
|
||||||
|
const filesNamesMap = req.files?.length
|
||||||
|
? makeFilesNamesMap(req.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||||
|
const { codePath, runTime } = await getRunTimeAndFilePath(program + '.js')
|
||||||
|
try {
|
||||||
|
const result = await new ExecutionController().executeFile({
|
||||||
|
programPath: codePath,
|
||||||
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
|
vars: vars,
|
||||||
|
otherArgs: otherArgs,
|
||||||
|
runTime,
|
||||||
|
session: req.sasjsSession,
|
||||||
|
forceStringResult: true
|
||||||
|
})
|
||||||
|
|
||||||
const content = await getMockResponseFromFile([
|
return {
|
||||||
process.cwd(),
|
content: result.result as string
|
||||||
'mocks',
|
}
|
||||||
...program.split('/')
|
} catch (err) {
|
||||||
])
|
console.log('err', err)
|
||||||
|
|
||||||
if (content.error) {
|
|
||||||
return content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedContent = parseJsonIfValid(content.content)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: parsedContent
|
content: 'No webout returned.'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +188,8 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'logged-in'
|
'logged-in'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -95,21 +198,27 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'login'
|
'login'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/SASLogon/login')
|
@Post('/SASLogon/login')
|
||||||
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||||
|
if (req.body.lt && req.body.lt !== 'validtoken')
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
|
||||||
this.loggedIn = req.body.username
|
this.loggedIn = req.body.username
|
||||||
|
|
||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'logged-in'
|
'logged-in'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -122,8 +231,8 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'public-access-denied'
|
'public-access-denied'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -131,8 +240,8 @@ export class MockSas9Controller {
|
|||||||
return await getMockResponseFromFile([
|
return await getMockResponseFromFile([
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'mocks',
|
'mocks',
|
||||||
'generic',
|
|
||||||
'sas9',
|
'sas9',
|
||||||
|
'generic',
|
||||||
'logged-out'
|
'logged-out'
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -152,23 +261,6 @@ export class MockSas9Controller {
|
|||||||
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
private isPublicAccount = () => this.loggedIn?.toLowerCase() === 'public'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If JSON is valid it will be parsed otherwise will return text unaltered
|
|
||||||
* @param content string to be parsed
|
|
||||||
* @returns JSON or string
|
|
||||||
*/
|
|
||||||
const parseJsonIfValid = (content: string) => {
|
|
||||||
let fileContent = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
fileContent = JSON.parse(content)
|
|
||||||
} catch (err: any) {
|
|
||||||
fileContent = content
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileContent
|
|
||||||
}
|
|
||||||
|
|
||||||
const getMockResponseFromFile = async (
|
const getMockResponseFromFile = async (
|
||||||
filePath: string[]
|
filePath: string[]
|
||||||
): Promise<MockFileRead> => {
|
): Promise<MockFileRead> => {
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ export const setupRoutes = (app: Express) => {
|
|||||||
appStreamRouter(req, res, next)
|
appStreamRouter(req, res, next)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use('/', csrfProtection, webRouter)
|
app.use('/', webRouter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import sas9WebRouter from './sas9-web'
|
|||||||
import sasViyaWebRouter from './sasviya-web'
|
import sasViyaWebRouter from './sasviya-web'
|
||||||
import webRouter from './web'
|
import webRouter from './web'
|
||||||
import { MOCK_SERVERTYPEType } from '../../utils'
|
import { MOCK_SERVERTYPEType } from '../../utils'
|
||||||
|
import { csrfProtection } from '../../middlewares'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ switch (MOCK_SERVERTYPE) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
router.use('/', webRouter)
|
router.use('/', csrfProtection, webRouter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,25 @@ import express from 'express'
|
|||||||
import { generateCSRFToken } from '../../middlewares'
|
import { generateCSRFToken } from '../../middlewares'
|
||||||
import { WebController } from '../../controllers'
|
import { WebController } from '../../controllers'
|
||||||
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||||
|
import multer from 'multer'
|
||||||
|
import path from 'path'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { FileUploadController } from '../../controllers/internal'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
const sas9WebRouter = express.Router()
|
const sas9WebRouter = express.Router()
|
||||||
const webController = new WebController()
|
const webController = new WebController()
|
||||||
// Mock controller must be singleton because it keeps the states
|
// Mock controller must be singleton because it keeps the states
|
||||||
// for example `isLoggedIn` and potentially more in future mocks
|
// for example `isLoggedIn` and potentially more in future mocks
|
||||||
const controller = new MockSas9Controller()
|
const controller = new MockSas9Controller()
|
||||||
|
const fileUploadController = new FileUploadController()
|
||||||
|
|
||||||
|
const mockPath = process.env.STATIC_MOCK_LOCATION || 'mocks'
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
dest: path.join(process.cwd(), mockPath, 'sas9', 'files-received')
|
||||||
|
})
|
||||||
|
|
||||||
sas9WebRouter.get('/', async (req, res) => {
|
sas9WebRouter.get('/', async (req, res) => {
|
||||||
let response
|
let response
|
||||||
@@ -27,7 +40,7 @@ sas9WebRouter.get('/', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||||
const response = await controller.sasStoredProcess()
|
const response = await controller.sasStoredProcess(req)
|
||||||
|
|
||||||
if (response.redirect) {
|
if (response.redirect) {
|
||||||
res.redirect(response.redirect)
|
res.redirect(response.redirect)
|
||||||
@@ -41,8 +54,8 @@ sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
|
sas9WebRouter.get('/SASStoredProcess/do/', async (req, res) => {
|
||||||
const response = await controller.sasStoredProcessDo(req)
|
const response = await controller.sasStoredProcessDoGet(req)
|
||||||
|
|
||||||
if (response.redirect) {
|
if (response.redirect) {
|
||||||
res.redirect(response.redirect)
|
res.redirect(response.redirect)
|
||||||
@@ -56,6 +69,26 @@ sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post(
|
||||||
|
'/SASStoredProcess/do/',
|
||||||
|
fileUploadController.preUploadMiddleware,
|
||||||
|
fileUploadController.getMulterUploadObject().any(),
|
||||||
|
async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcessDoPost(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
|
||||||
const response = await controller.loginGet()
|
const response = await controller.loginGet()
|
||||||
|
|
||||||
|
|||||||
1
api/src/types/system/process.d.ts
vendored
1
api/src/types/system/process.d.ts
vendored
@@ -5,6 +5,7 @@ declare namespace NodeJS {
|
|||||||
pythonLoc?: string
|
pythonLoc?: string
|
||||||
rLoc?: string
|
rLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
|
sasjsRoot: string
|
||||||
logsLoc: string
|
logsLoc: string
|
||||||
logsUUID: string
|
logsUUID: string
|
||||||
sessionController?: import('../../controllers/internal').SessionController
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { getMacrosFolder, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
|||||||
export const copySASjsCore = async () => {
|
export const copySASjsCore = async () => {
|
||||||
if (process.env.NODE_ENV === 'test') return
|
if (process.env.NODE_ENV === 'test') return
|
||||||
|
|
||||||
console.log('Copying Macros from container to drive(tmp).')
|
console.log('Copying Macros from container to drive.')
|
||||||
|
|
||||||
const macrosDrivePath = getMacrosFolder()
|
const macrosDrivePath = getMacrosFolder()
|
||||||
|
|
||||||
|
|||||||
@@ -20,22 +20,24 @@ export const getSasjsHomeFolder = () => path.join(homedir(), '.sasjs-server')
|
|||||||
export const getDesktopUserAutoExecPath = () =>
|
export const getDesktopUserAutoExecPath = () =>
|
||||||
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
path.join(getSasjsHomeFolder(), 'user-autoexec.sas')
|
||||||
|
|
||||||
export const getSasjsRootFolder = () => process.driveLoc
|
export const getSasjsRootFolder = () => process.sasjsRoot
|
||||||
|
|
||||||
|
export const getSasjsDriveFolder = () => process.driveLoc
|
||||||
|
|
||||||
export const getLogFolder = () => process.logsLoc
|
export const getLogFolder = () => process.logsLoc
|
||||||
|
|
||||||
export const getAppStreamConfigPath = () =>
|
export const getAppStreamConfigPath = () =>
|
||||||
path.join(getSasjsRootFolder(), 'appStreamConfig.json')
|
path.join(getSasjsDriveFolder(), 'appStreamConfig.json')
|
||||||
|
|
||||||
export const getMacrosFolder = () =>
|
export const getMacrosFolder = () =>
|
||||||
path.join(getSasjsRootFolder(), 'sas', 'sasautos')
|
path.join(getSasjsDriveFolder(), 'sas', 'sasautos')
|
||||||
|
|
||||||
export const getPackagesFolder = () =>
|
export const getPackagesFolder = () =>
|
||||||
path.join(getSasjsRootFolder(), 'sas', 'sas_packages')
|
path.join(getSasjsDriveFolder(), 'sas', 'sas_packages')
|
||||||
|
|
||||||
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
export const getUploadsFolder = () => path.join(getSasjsRootFolder(), 'uploads')
|
||||||
|
|
||||||
export const getFilesFolder = () => path.join(getSasjsRootFolder(), 'files')
|
export const getFilesFolder = () => path.join(getSasjsDriveFolder(), 'files')
|
||||||
|
|
||||||
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
export const getWeboutFolder = () => path.join(getSasjsRootFolder(), 'webouts')
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,12 @@ export const getPreProgramVariables = (req: Request): PreProgramVars => {
|
|||||||
|
|
||||||
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
if (cookies.length) httpHeaders.push(`cookie: ${cookies.join('; ')}`)
|
||||||
|
|
||||||
|
//In desktop mode when mocking mode is enabled, user was undefined.
|
||||||
|
//So this is workaround.
|
||||||
return {
|
return {
|
||||||
username: user!.username,
|
username: user ? user.username : 'demo',
|
||||||
userId: user!.userId,
|
userId: user ? user.userId : 0,
|
||||||
displayName: user!.displayName,
|
displayName: user ? user.displayName : 'demo',
|
||||||
serverUrl: protocol + host,
|
serverUrl: protocol + host,
|
||||||
httpHeaders
|
httpHeaders
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export * from './saveTokensInDB'
|
|||||||
export * from './seedDB'
|
export * from './seedDB'
|
||||||
export * from './setProcessVariables'
|
export * from './setProcessVariables'
|
||||||
export * from './setupFolders'
|
export * from './setupFolders'
|
||||||
|
export * from './setupUserAutoExec'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './validation'
|
export * from './validation'
|
||||||
export * from './verifyEnvVariables'
|
export * from './verifyEnvVariables'
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export const setProcessVariables = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root')
|
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
|
||||||
|
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +33,6 @@ export const setProcessVariables = async () => {
|
|||||||
process.rLoc = process.env.R_PATH
|
process.rLoc = process.env.R_PATH
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
process.sasLoc = sasLoc
|
||||||
process.nodeLoc = nodeLoc
|
process.nodeLoc = nodeLoc
|
||||||
process.pythonLoc = pythonLoc
|
process.pythonLoc = pythonLoc
|
||||||
@@ -42,11 +42,19 @@ export const setProcessVariables = async () => {
|
|||||||
const { SASJS_ROOT } = process.env
|
const { SASJS_ROOT } = process.env
|
||||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||||
await createFolder(absPath)
|
await createFolder(absPath)
|
||||||
process.driveLoc = getRealPath(absPath)
|
process.sasjsRoot = getRealPath(absPath)
|
||||||
|
|
||||||
|
const { DRIVE_LOCATION } = process.env
|
||||||
|
const absDrivePath = getAbsolutePath(
|
||||||
|
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
|
||||||
|
process.cwd()
|
||||||
|
)
|
||||||
|
await createFolder(absDrivePath)
|
||||||
|
process.driveLoc = getRealPath(absDrivePath)
|
||||||
|
|
||||||
const { LOG_LOCATION } = process.env
|
const { LOG_LOCATION } = process.env
|
||||||
const absLogsPath = getAbsolutePath(
|
const absLogsPath = getAbsolutePath(
|
||||||
LOG_LOCATION ?? `sasjs_root${path.sep}logs`,
|
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
|
||||||
process.cwd()
|
process.cwd()
|
||||||
)
|
)
|
||||||
await createFolder(absLogsPath)
|
await createFolder(absLogsPath)
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
import { createFile, createFolder, fileExists } from '@sasjs/utils'
|
import { createFolder } from '@sasjs/utils'
|
||||||
import {
|
import { getFilesFolder, getPackagesFolder } from './file'
|
||||||
getDesktopUserAutoExecPath,
|
|
||||||
getFilesFolder,
|
|
||||||
getPackagesFolder
|
|
||||||
} from './file'
|
|
||||||
import { ModeType } from './verifyEnvVariables'
|
|
||||||
|
|
||||||
export const setupFolders = async () => {
|
export const setupFolders = async () => {
|
||||||
const drivePath = getFilesFolder()
|
await createFolder(getFilesFolder())
|
||||||
await createFolder(drivePath)
|
|
||||||
await createFolder(getPackagesFolder())
|
await createFolder(getPackagesFolder())
|
||||||
|
|
||||||
if (process.env.MODE === ModeType.Desktop) {
|
|
||||||
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
|
||||||
await createFile(getDesktopUserAutoExecPath(), '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
api/src/utils/setupUserAutoExec.ts
Normal file
11
api/src/utils/setupUserAutoExec.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createFile, fileExists } from '@sasjs/utils'
|
||||||
|
import { getDesktopUserAutoExecPath } from './file'
|
||||||
|
import { ModeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
|
export const setupUserAutoExec = async () => {
|
||||||
|
if (process.env.MODE === ModeType.Desktop) {
|
||||||
|
if (!(await fileExists(getDesktopUserAutoExecPath()))) {
|
||||||
|
await createFile(getDesktopUserAutoExecPath(), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,14 +31,24 @@ const DeleteConfirmationModal = ({
|
|||||||
message,
|
message,
|
||||||
_delete
|
_delete
|
||||||
}: DeleteConfirmationModalProps) => {
|
}: DeleteConfirmationModalProps) => {
|
||||||
|
const handleDeleteClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
_delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog onClose={handleClose} open={open}>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography gutterBottom>{message}</Typography>
|
<Typography gutterBottom>{message}</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
<Button color="error" onClick={() => _delete()}>
|
<Button color="error" onClick={handleDeleteClick}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -69,8 +69,18 @@ const NameInputModal = ({
|
|||||||
action(name)
|
action(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleActionClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
action(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog fullWidth onClose={handleClose} open={open}>
|
||||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
{title}
|
{title}
|
||||||
</BootstrapDialogTitle>
|
</BootstrapDialogTitle>
|
||||||
@@ -91,12 +101,12 @@ const NameInputModal = ({
|
|||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
<Button variant="contained" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => action(name)}
|
onClick={handleActionClick}
|
||||||
disabled={hasError || !name}
|
disabled={hasError || !name}
|
||||||
>
|
>
|
||||||
{actionLabel}
|
{actionLabel}
|
||||||
|
|||||||
@@ -1,67 +1,79 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Menu, MenuItem } from '@mui/material'
|
import { Menu, MenuItem, Typography } from '@mui/material'
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||||
|
import MuiTreeView from '@mui/lab/TreeView'
|
||||||
|
import MuiTreeItem from '@mui/lab/TreeItem'
|
||||||
|
|
||||||
import DeleteConfirmationModal from './deleteConfirmationModal'
|
import DeleteConfirmationModal from './deleteConfirmationModal'
|
||||||
import NameInputModal from './nameInputModal'
|
import NameInputModal from './nameInputModal'
|
||||||
|
|
||||||
import { TreeNode } from '../utils/types'
|
import { TreeNode } from '../utils/types'
|
||||||
|
|
||||||
type Props = {
|
interface Props {
|
||||||
node: TreeNode
|
node: TreeNode
|
||||||
selectedFilePath: string
|
|
||||||
handleSelect: (filePath: string) => void
|
handleSelect: (filePath: string) => void
|
||||||
deleteNode: (path: string, isFolder: boolean) => void
|
deleteNode: (path: string, isFolder: boolean) => void
|
||||||
addFile: (path: string) => void
|
addFile: (path: string) => void
|
||||||
addFolder: (path: string) => void
|
addFolder: (path: string) => void
|
||||||
rename: (oldPath: string, newPath: string) => void
|
rename: (oldPath: string, newPath: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeViewProps extends Props {
|
||||||
defaultExpanded?: string[]
|
defaultExpanded?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const TreeView = ({
|
const TreeView = ({
|
||||||
node,
|
node,
|
||||||
selectedFilePath,
|
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
addFile,
|
addFile,
|
||||||
addFolder,
|
addFolder,
|
||||||
rename,
|
rename,
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
}: Props) => {
|
}: TreeViewProps) => {
|
||||||
return (
|
const renderTree = (nodes: TreeNode) => (
|
||||||
<ul
|
<MuiTreeItem
|
||||||
style={{
|
key={nodes.relativePath}
|
||||||
listStyle: 'none',
|
nodeId={nodes.relativePath}
|
||||||
padding: '0.25rem 0.85rem',
|
label={
|
||||||
width: 'max-content'
|
<TreeItemWithContextMenu
|
||||||
}}
|
node={nodes}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
|
rename={rename}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TreeViewNode
|
{Array.isArray(nodes.children)
|
||||||
node={node}
|
? nodes.children.map((node) => renderTree(node))
|
||||||
selectedFilePath={selectedFilePath}
|
: null}
|
||||||
handleSelect={handleSelect}
|
</MuiTreeItem>
|
||||||
deleteNode={deleteNode}
|
)
|
||||||
addFile={addFile}
|
|
||||||
addFolder={addFolder}
|
return (
|
||||||
rename={rename}
|
<MuiTreeView
|
||||||
defaultExpanded={defaultExpanded}
|
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||||
/>
|
defaultExpandIcon={<ChevronRightIcon />}
|
||||||
</ul>
|
defaultExpanded={defaultExpanded}
|
||||||
|
sx={{ flexGrow: 1, maxWidth: 400, overflowY: 'auto' }}
|
||||||
|
>
|
||||||
|
{renderTree(node)}
|
||||||
|
</MuiTreeView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TreeView
|
export default TreeView
|
||||||
|
|
||||||
const TreeViewNode = ({
|
const TreeItemWithContextMenu = ({
|
||||||
node,
|
node,
|
||||||
selectedFilePath,
|
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
addFile,
|
addFile,
|
||||||
addFolder,
|
addFolder,
|
||||||
rename,
|
rename
|
||||||
defaultExpanded
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
@@ -72,18 +84,19 @@ const TreeViewNode = ({
|
|||||||
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
||||||
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
||||||
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||||
const [childVisible, setChildVisibility] = useState(false)
|
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
mouseX: number
|
mouseX: number
|
||||||
mouseY: number
|
mouseY: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const launchProgram = () => {
|
const launchProgram = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchProgramWithDebug = () => {
|
const launchProgramWithDebug = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
window.open(
|
window.open(
|
||||||
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
||||||
@@ -103,25 +116,18 @@ const TreeViewNode = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChild = node.children.length ? true : false
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
const handleItemClick = () => {
|
setContextMenu(null)
|
||||||
if (node.children.length) {
|
}
|
||||||
setChildVisibility((v) => !v)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleItemClick = (event: React.MouseEvent) => {
|
||||||
|
if (node.children.length) return
|
||||||
handleSelect(node.relativePath)
|
handleSelect(node.relativePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const handleDeleteItemClick = (event: React.MouseEvent) => {
|
||||||
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
|
event.stopPropagation()
|
||||||
setChildVisibility(true)
|
|
||||||
defaultExpanded.shift()
|
|
||||||
}
|
|
||||||
}, [defaultExpanded, node.relativePath])
|
|
||||||
|
|
||||||
const handleDeleteItemClick = () => {
|
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setDeleteConfirmationModalOpen(true)
|
setDeleteConfirmationModalOpen(true)
|
||||||
setDeleteConfirmationModalMessage(
|
setDeleteConfirmationModalMessage(
|
||||||
@@ -136,7 +142,8 @@ const TreeViewNode = ({
|
|||||||
deleteNode(node.relativePath, node.isFolder)
|
deleteNode(node.relativePath, node.isFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewFolderItemClick = () => {
|
const handleNewFolderItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Add Folder')
|
setNameInputModalTitle('Add Folder')
|
||||||
@@ -145,7 +152,8 @@ const TreeViewNode = ({
|
|||||||
setDefaultInputModalName('')
|
setDefaultInputModalName('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewFileItemClick = () => {
|
const handleNewFileItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Add File')
|
setNameInputModalTitle('Add File')
|
||||||
@@ -161,7 +169,8 @@ const TreeViewNode = ({
|
|||||||
else addFile(path)
|
else addFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRenameItemClick = () => {
|
const handleRenameItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Rename')
|
setNameInputModalTitle('Rename')
|
||||||
@@ -181,34 +190,7 @@ const TreeViewNode = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||||
<li style={{ display: 'list-item' }}>
|
<Typography onClick={handleItemClick}>{node.name}</Typography>
|
||||||
<div
|
|
||||||
className={`tree-item-label ${
|
|
||||||
selectedFilePath === node.relativePath ? 'selected' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => handleItemClick()}
|
|
||||||
>
|
|
||||||
{hasChild &&
|
|
||||||
(childVisible ? <ExpandMoreIcon /> : <ChevronRightIcon />)}
|
|
||||||
<div>{node.name}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasChild &&
|
|
||||||
childVisible &&
|
|
||||||
node.children.map((child, index) => (
|
|
||||||
<TreeView
|
|
||||||
key={node.relativePath + '-' + index}
|
|
||||||
node={child}
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
handleSelect={handleSelect}
|
|
||||||
deleteNode={deleteNode}
|
|
||||||
addFile={addFile}
|
|
||||||
addFolder={addFolder}
|
|
||||||
rename={rename}
|
|
||||||
defaultExpanded={defaultExpanded}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</li>
|
|
||||||
<DeleteConfirmationModal
|
<DeleteConfirmationModal
|
||||||
open={deleteConfirmationModalOpen}
|
open={deleteConfirmationModalOpen}
|
||||||
setOpen={setDeleteConfirmationModalOpen}
|
setOpen={setDeleteConfirmationModalOpen}
|
||||||
@@ -228,7 +210,7 @@ const TreeViewNode = ({
|
|||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
open={contextMenu !== null}
|
open={contextMenu !== null}
|
||||||
onClose={() => setContextMenu(null)}
|
onClose={handleClose}
|
||||||
anchorReference="anchorPosition"
|
anchorReference="anchorPosition"
|
||||||
anchorPosition={
|
anchorPosition={
|
||||||
contextMenu !== null
|
contextMenu !== null
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const SASjsEditor = ({
|
|||||||
setTab
|
setTab
|
||||||
}: SASjsEditorProps) => {
|
}: SASjsEditorProps) => {
|
||||||
const {
|
const {
|
||||||
ctrlPressed,
|
|
||||||
fileContent,
|
fileContent,
|
||||||
isLoading,
|
isLoading,
|
||||||
log,
|
log,
|
||||||
@@ -64,8 +63,6 @@ const SASjsEditor = ({
|
|||||||
handleDiffEditorDidMount,
|
handleDiffEditorDidMount,
|
||||||
handleEditorDidMount,
|
handleEditorDidMount,
|
||||||
handleFilePathInput,
|
handleFilePathInput,
|
||||||
handleKeyDown,
|
|
||||||
handleKeyUp,
|
|
||||||
handleRunBtnClick,
|
handleRunBtnClick,
|
||||||
handleTabChange,
|
handleTabChange,
|
||||||
saveFile,
|
saveFile,
|
||||||
@@ -99,7 +96,6 @@ const SASjsEditor = ({
|
|||||||
original={prevFileContent}
|
original={prevFileContent}
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
editorDidMount={handleDiffEditorDidMount}
|
editorDidMount={handleDiffEditorDidMount}
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
onChange={(val) => setFileContent(val)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -108,7 +104,6 @@ const SASjsEditor = ({
|
|||||||
language={getLanguageFromExtension(selectedFileExtension)}
|
language={getLanguageFromExtension(selectedFileExtension)}
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
editorDidMount={handleEditorDidMount}
|
editorDidMount={handleEditorDidMount}
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
onChange={(val) => setFileContent(val)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -176,8 +171,6 @@ const SASjsEditor = ({
|
|||||||
{fileMenu}
|
{fileMenu}
|
||||||
</Box>
|
</Box>
|
||||||
<Paper
|
<Paper
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
sx={{
|
sx={{
|
||||||
height: 'calc(100vh - 170px)',
|
height: 'calc(100vh - 170px)',
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ const useEditor = ({
|
|||||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||||
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 [runTimes, setRunTimes] = useState<string[]>([])
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
@@ -148,53 +147,47 @@ const useEditor = ({
|
|||||||
const handleRunBtnClick = () =>
|
const handleRunBtnClick = () =>
|
||||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||||
|
|
||||||
const runCode = (code: string) => {
|
const runCode = useCallback(
|
||||||
setIsLoading(true)
|
(code: string) => {
|
||||||
axios
|
setIsLoading(true)
|
||||||
.post(`/SASjsApi/code/execute`, {
|
axios
|
||||||
code: programPathInjection(
|
.post(`/SASjsApi/code/execute`, {
|
||||||
code,
|
code: programPathInjection(
|
||||||
selectedFilePath,
|
code,
|
||||||
selectedRunTime as RunTimeType
|
selectedFilePath,
|
||||||
),
|
selectedRunTime as RunTimeType
|
||||||
runTime: selectedRunTime
|
),
|
||||||
})
|
runTime: selectedRunTime
|
||||||
.then((res: any) => {
|
})
|
||||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
.then((res: any) => {
|
||||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||||
setTab('log')
|
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||||
|
setTab('log')
|
||||||
|
|
||||||
// Scroll to bottom of log
|
// Scroll to bottom of log
|
||||||
const logElement = document.getElementById('log')
|
const logElement = document.getElementById('log')
|
||||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setModalTitle('Abort')
|
setModalTitle('Abort')
|
||||||
setModalPayload(
|
setModalPayload(
|
||||||
typeof err.response.data === 'object'
|
typeof err.response.data === 'object'
|
||||||
? JSON.stringify(err.response.data)
|
? JSON.stringify(err.response.data)
|
||||||
: err.response.data
|
: err.response.data
|
||||||
)
|
)
|
||||||
setOpenModal(true)
|
setOpenModal(true)
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false))
|
.finally(() => setIsLoading(false))
|
||||||
}
|
},
|
||||||
|
[
|
||||||
const handleKeyDown = (event: any) => {
|
selectedFilePath,
|
||||||
if (event.ctrlKey) {
|
selectedRunTime,
|
||||||
if (event.key === 'v') {
|
setModalPayload,
|
||||||
setCtrlPressed(false)
|
setModalTitle,
|
||||||
}
|
setOpenModal,
|
||||||
|
setTab
|
||||||
if (event.key === 'Enter')
|
]
|
||||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
)
|
||||||
if (!ctrlPressed) setCtrlPressed(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyUp = (event: any) => {
|
|
||||||
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||||
setSelectedRunTime(event.target.value as RunTimeType)
|
setSelectedRunTime(event.target.value as RunTimeType)
|
||||||
@@ -223,7 +216,28 @@ const useEditor = ({
|
|||||||
if (prevFileContent !== fileContent) return saveFile()
|
if (prevFileContent !== fileContent) return saveFile()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
|
|
||||||
|
editorRef.current.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'run-code',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Run Code',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
||||||
|
|
||||||
|
contextMenuGroupId: 'navigation',
|
||||||
|
|
||||||
|
contextMenuOrder: 1,
|
||||||
|
|
||||||
|
// Method that will be executed when the action is triggered.
|
||||||
|
// @param editor The editor instance is passed in as a convenience
|
||||||
|
run: function () {
|
||||||
|
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [fileContent, prevFileContent, selectedFilePath, saveFile, runCode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
@@ -277,7 +291,6 @@ const useEditor = ({
|
|||||||
}, [selectedFileExtension, runTimes])
|
}, [selectedFileExtension, runTimes])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ctrlPressed,
|
|
||||||
fileContent,
|
fileContent,
|
||||||
isLoading,
|
isLoading,
|
||||||
log,
|
log,
|
||||||
@@ -293,8 +306,6 @@ const useEditor = ({
|
|||||||
handleDiffEditorDidMount,
|
handleDiffEditorDidMount,
|
||||||
handleEditorDidMount,
|
handleEditorDidMount,
|
||||||
handleFilePathInput,
|
handleFilePathInput,
|
||||||
handleKeyDown,
|
|
||||||
handleKeyUp,
|
|
||||||
handleRunBtnClick,
|
handleRunBtnClick,
|
||||||
handleTabChange,
|
handleTabChange,
|
||||||
saveFile,
|
saveFile,
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ const SideBar = ({
|
|||||||
{directoryData && (
|
{directoryData && (
|
||||||
<TreeView
|
<TreeView
|
||||||
node={directoryData}
|
node={directoryData}
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
handleSelect={handleFileSelect}
|
handleSelect={handleFileSelect}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
addFile={addFile}
|
addFile={addFile}
|
||||||
|
|||||||
Reference in New Issue
Block a user