mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b88c911527 | ||
|
|
8b12f31060 | ||
|
|
e65cba9af0 | ||
|
|
a9c9b734f5 | ||
|
|
39da41c9f1 | ||
| 662b2ca36a | |||
| 16b7aa6abb | |||
| 4560ef942f | |||
| 06d3b17154 | |||
| d6651bbdbe | |||
| b9d032f148 | |||
|
|
70655e74d3 | ||
|
|
cb82fea0d8 | ||
| b9a596616d | |||
|
|
72a5393be3 | ||
|
|
769a840e9f | ||
| 730c7c52ac | |||
| ee2db276bb | |||
|
|
d0a24aacb6 | ||
|
|
57dfdf89a4 | ||
|
|
393b5eaf99 | ||
|
|
7477326b22 | ||
|
|
76bf84316e | ||
|
|
e355276e44 | ||
|
|
a3a9e3bd9f | ||
|
|
9f06080348 | ||
|
|
4bbf9cfdb3 | ||
|
|
e8e71fcde9 | ||
|
|
e63271a67a | ||
| 7633608318 | |||
|
|
e67d27d264 | ||
|
|
53033ccc96 | ||
|
|
6131ed1cbe | ||
|
|
5d624e3399 | ||
| ee17d37aa1 | |||
| 572fe22d50 | |||
| 091268bf58 | |||
| 71a4a48443 | |||
| 3b188cd724 | |||
| eeba2328c0 | |||
| 0a0ba2cca5 | |||
|
|
476f834a80 | ||
|
|
8b8739a873 | ||
| bce83cb6fb | |||
| 3a3c90d9e6 | |||
|
|
e63eaa5302 | ||
|
|
65de1bb175 | ||
|
|
a5ee2f2923 | ||
| 98ea2ac9b9 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ node_modules/
|
|||||||
.env*
|
.env*
|
||||||
sas/
|
sas/
|
||||||
sasjs_root/
|
sasjs_root/
|
||||||
|
api/mocks/custom/*
|
||||||
|
!api/mocks/custom/.keep
|
||||||
tmp/
|
tmp/
|
||||||
build/
|
build/
|
||||||
sasjsbuild/
|
sasjsbuild/
|
||||||
|
|||||||
82
CHANGELOG.md
82
CHANGELOG.md
@@ -1,3 +1,85 @@
|
|||||||
|
# [0.21.0](https://github.com/sasjs/server/compare/v0.20.0...v0.21.0) (2022-09-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* sas9 mocker improved - public access denied scenario ([06d3b17](https://github.com/sasjs/server/commit/06d3b1715432ea245ee755ae1dfd0579d3eb30e9))
|
||||||
|
|
||||||
|
# [0.20.0](https://github.com/sasjs/server/compare/v0.19.0...v0.20.0) (2022-09-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add support for R stored programs ([d6651bb](https://github.com/sasjs/server/commit/d6651bbdbeee5067f53c36e69a0eefa973c523b6))
|
||||||
|
|
||||||
|
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
|
||||||
|
|
||||||
|
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add option for program launch in context menu ([ee2db27](https://github.com/sasjs/server/commit/ee2db276bb0bbd522f758e0b66f7e7b2f4afd9d5))
|
||||||
|
|
||||||
|
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](https://github.com/sasjs/server/commit/393b5eaf990049c39eecf2b9e8dd21a001b6e298))
|
||||||
|
|
||||||
|
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* invalid JS logic ([9f06080](https://github.com/sasjs/server/commit/9f06080348aed076f8188a26fb4890d38a5a3510))
|
||||||
|
|
||||||
|
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* making SASINITIALFOLDER option windows only. Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](https://github.com/sasjs/server/commit/e63271a67a0deb3059a5f2bec1854efee5a6e5a5))
|
||||||
|
|
||||||
|
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* addition of SASINITIALFOLDER startup option. Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](https://github.com/sasjs/server/commit/a5ee2f292384f90e9d95d003d652311c0d91a7a7))
|
||||||
|
|
||||||
|
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo mistake ([ee17d37](https://github.com/sasjs/server/commit/ee17d37aa188b0ca43cea0e89d6cd1a566b765cb))
|
||||||
|
|
||||||
|
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow underscores in file name ([bce83cb](https://github.com/sasjs/server/commit/bce83cb6fbc98f8198564c9399821f5829acc767))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add the functionality of saving file by ctrl + s in editor ([3a3c90d](https://github.com/sasjs/server/commit/3a3c90d9e690ac5267bf1acc834b5b5c5b4dadb6))
|
||||||
|
|
||||||
|
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](https://github.com/sasjs/server/commit/98ea2ac9b98631605e39e5900e533727ea0e3d85))
|
||||||
|
|
||||||
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)
|
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -66,14 +66,14 @@ MODE=
|
|||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
# A comma separated string that defines the available runTimes.
|
||||||
# Priority is given to the runtime that comes first in the string.
|
# Priority is given to the runtime that comes first in the string.
|
||||||
# Possible options at the moment are sas and js
|
# Possible options at the moment are sas, js, py and r
|
||||||
|
|
||||||
# This string sets the priority of the available analytic runtimes
|
# This string sets the priority of the available analytic runtimes
|
||||||
# Valid runtimes are SAS (sas), JavaScript (js) and Python (py)
|
# Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r)
|
||||||
# For each option provided, there should be a corresponding path,
|
# For each option provided, there should be a corresponding path,
|
||||||
# eg SAS_PATH, NODE_PATH or PYTHON_PATH
|
# eg SAS_PATH, NODE_PATH, PYTHON_PATH or RSCRIPT_PATH
|
||||||
# Priority is given to runtimes earlier in the string
|
# Priority is given to runtimes earlier in the string
|
||||||
# Example options: [sas,js,py | js,py | sas | sas,js]
|
# Example options: [sas,js,py | js,py | sas | sas,js | r | sas,r]
|
||||||
RUN_TIMES=
|
RUN_TIMES=
|
||||||
|
|
||||||
# Path to SAS executable (sas.exe / sas.sh)
|
# Path to SAS executable (sas.exe / sas.sh)
|
||||||
@@ -85,6 +85,9 @@ NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
|||||||
# Path to Python executable
|
# Path to Python executable
|
||||||
PYTHON_PATH=/usr/bin/python
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
|
||||||
|
# Path to R executable
|
||||||
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
# Path to working directory
|
# Path to working directory
|
||||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
@@ -96,6 +99,9 @@ PROTOCOL=
|
|||||||
# default: 5000
|
# default: 5000
|
||||||
PORT=
|
PORT=
|
||||||
|
|
||||||
|
# options: [sas9|sasviya]
|
||||||
|
# If not present, mocking function is disabled
|
||||||
|
MOCK_SERVERTYPE=
|
||||||
|
|
||||||
#
|
#
|
||||||
## Additional SAS Options
|
## Additional SAS Options
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
|||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
PYTHON_PATH=/usr/bin/python
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
R_PATH=/usr/bin/Rscript
|
||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
|||||||
0
api/mocks/custom/.keep
Normal file
0
api/mocks/custom/.keep
Normal file
1
api/mocks/generic/sas9/logged-in
Normal file
1
api/mocks/generic/sas9/logged-in
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed in.
|
||||||
1
api/mocks/generic/sas9/logged-out
Normal file
1
api/mocks/generic/sas9/logged-out
Normal file
@@ -0,0 +1 @@
|
|||||||
|
You have signed out.
|
||||||
30
api/mocks/generic/sas9/login
Normal file
30
api/mocks/generic/sas9/login
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
<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 container-->
|
||||||
|
<input type="hidden" name="lt" value="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" aria-hidden="true" />
|
||||||
|
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
|
||||||
|
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
|
||||||
|
|
||||||
|
<span class="userid">
|
||||||
|
|
||||||
|
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
|
||||||
|
</span>
|
||||||
|
<span class="password">
|
||||||
|
|
||||||
|
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</html>
|
||||||
1
api/mocks/generic/sas9/public-access-denied
Normal file
1
api/mocks/generic/sas9/public-access-denied
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Public access has been denied.
|
||||||
1
api/mocks/generic/sas9/sas-stored-process
Normal file
1
api/mocks/generic/sas9/sas-stored-process
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"title": "Log Off SAS Demo User"
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,10 @@ export default setProcessVariables().then(async () => {
|
|||||||
app.use(express.json({ limit: '100mb' }))
|
app.use(express.json({ limit: '100mb' }))
|
||||||
app.use(express.static(path.join(__dirname, '../public')))
|
app.use(express.static(path.join(__dirname, '../public')))
|
||||||
|
|
||||||
|
// Body parser is used for decoding the formdata on POST request.
|
||||||
|
// Currently only place we use it is SAS9 Mock - POST /SASLogon/login
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
await setupFolders()
|
await setupFolders()
|
||||||
await copySASjsCore()
|
await copySASjsCore()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
import { ExecutionController } from './internal'
|
||||||
import { ExecuteReturnJsonResponse } from '.'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
getUserAutoExec,
|
getUserAutoExec,
|
||||||
@@ -35,7 +34,7 @@ export class CodeController {
|
|||||||
public async executeCode(
|
public async executeCode(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body: ExecuteCodePayload
|
@Body() body: ExecuteCodePayload
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
return executeCode(request, body)
|
return executeCode(request, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,22 +50,15 @@ const executeCode = async (
|
|||||||
: await getUserAutoExec()
|
: await getUserAutoExec()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { result } = await new ExecutionController().executeProgram({
|
||||||
(await new ExecutionController().executeProgram({
|
program: code,
|
||||||
program: code,
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
vars: { ...req.query, _debug: 131 },
|
||||||
vars: { ...req.query, _debug: 131 },
|
otherArgs: { userAutoExec },
|
||||||
otherArgs: { userAutoExec },
|
runTime: runTime
|
||||||
returnJson: true,
|
})
|
||||||
runTime: runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
return {
|
return result
|
||||||
status: 'success',
|
|
||||||
_webout: webout as string,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ export interface ExecuteReturnRaw {
|
|||||||
result: string | Buffer
|
result: string | Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteReturnJson {
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
webout: string | Buffer
|
|
||||||
log?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecuteFileParams {
|
interface ExecuteFileParams {
|
||||||
programPath: string
|
programPath: string
|
||||||
preProgramVariables: PreProgramVars
|
preProgramVariables: PreProgramVars
|
||||||
@@ -68,10 +62,9 @@ export class ExecutionController {
|
|||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
|
||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController = getSessionController(runTime)
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
@@ -89,8 +82,6 @@ export class ExecutionController {
|
|||||||
tokenFile,
|
tokenFile,
|
||||||
preProgramVariables?.httpHeaders.join('\n') ?? ''
|
preProgramVariables?.httpHeaders.join('\n') ?? ''
|
||||||
)
|
)
|
||||||
if (returnJson)
|
|
||||||
await createFile(headersPath, 'Content-type: application/json')
|
|
||||||
|
|
||||||
await processProgram(
|
await processProgram(
|
||||||
program,
|
program,
|
||||||
@@ -110,10 +101,7 @@ export class ExecutionController {
|
|||||||
? await readFile(headersPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
const fileResponse: boolean =
|
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||||
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
|
||||||
@@ -124,19 +112,11 @@ export class ExecutionController {
|
|||||||
// it should be deleted by scheduleSessionDestroy
|
// it should be deleted by scheduleSessionDestroy
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
|
|
||||||
if (returnJson) {
|
|
||||||
return {
|
|
||||||
httpHeaders,
|
|
||||||
webout,
|
|
||||||
log: isDebugOn(vars) || session.crashed ? log : undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpHeaders,
|
httpHeaders,
|
||||||
result:
|
result:
|
||||||
isDebugOn(vars) || session.crashed
|
isDebugOn(vars) || session.crashed
|
||||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
? `${webout}\n${process.logsUUID}\n${log}`
|
||||||
: webout
|
: webout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,41 @@ import {
|
|||||||
|
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
|
|
||||||
abstract class SessionController {
|
export class SessionController {
|
||||||
protected sessions: Session[] = []
|
protected sessions: Session[] = []
|
||||||
|
|
||||||
protected getReadySessions = (): Session[] =>
|
protected getReadySessions = (): Session[] =>
|
||||||
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
this.sessions.filter((sess: Session) => sess.ready && !sess.consumed)
|
||||||
|
|
||||||
protected abstract createSession(): Promise<Session>
|
protected async createSession(): Promise<Session> {
|
||||||
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
|
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||||
|
|
||||||
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
// death time of session is 15 mins from creation
|
||||||
|
const deathTimeStamp = (
|
||||||
|
parseInt(creationTimeStamp) +
|
||||||
|
15 * 60 * 1000 -
|
||||||
|
1000
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
const session: Session = {
|
||||||
|
id: sessionId,
|
||||||
|
ready: true,
|
||||||
|
inUse: true,
|
||||||
|
consumed: false,
|
||||||
|
completed: false,
|
||||||
|
creationTimeStamp,
|
||||||
|
deathTimeStamp,
|
||||||
|
path: sessionFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
|
await createFile(headersPath, 'Content-type: text/plain')
|
||||||
|
|
||||||
|
this.sessions.push(session)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
public async getSession() {
|
public async getSession() {
|
||||||
const readySessions = this.getReadySessions()
|
const readySessions = this.getReadySessions()
|
||||||
@@ -101,12 +129,14 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
|
isWindows() ? '-nologo' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
|
||||||
isWindows() ? '-nologo' : ''
|
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
|
||||||
|
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
|
||||||
])
|
])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
session.completed = true
|
session.completed = true
|
||||||
@@ -165,110 +195,17 @@ ${autoExecContent}`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JSSessionController extends SessionController {
|
|
||||||
protected async createSession(): Promise<Session> {
|
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
|
||||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
|
||||||
// death time of session is 15 mins from creation
|
|
||||||
const deathTimeStamp = (
|
|
||||||
parseInt(creationTimeStamp) +
|
|
||||||
15 * 60 * 1000 -
|
|
||||||
1000
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
const session: Session = {
|
|
||||||
id: sessionId,
|
|
||||||
ready: true,
|
|
||||||
inUse: true,
|
|
||||||
consumed: false,
|
|
||||||
completed: false,
|
|
||||||
creationTimeStamp,
|
|
||||||
deathTimeStamp,
|
|
||||||
path: sessionFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
|
||||||
await createFile(headersPath, 'Content-type: text/plain')
|
|
||||||
|
|
||||||
this.sessions.push(session)
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PythonSessionController extends SessionController {
|
|
||||||
protected async createSession(): Promise<Session> {
|
|
||||||
const sessionId = generateUniqueFileName(generateTimestamp())
|
|
||||||
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
|
||||||
|
|
||||||
const creationTimeStamp = sessionId.split('-').pop() as string
|
|
||||||
// death time of session is 15 mins from creation
|
|
||||||
const deathTimeStamp = (
|
|
||||||
parseInt(creationTimeStamp) +
|
|
||||||
15 * 60 * 1000 -
|
|
||||||
1000
|
|
||||||
).toString()
|
|
||||||
|
|
||||||
const session: Session = {
|
|
||||||
id: sessionId,
|
|
||||||
ready: true,
|
|
||||||
inUse: true,
|
|
||||||
consumed: false,
|
|
||||||
completed: false,
|
|
||||||
creationTimeStamp,
|
|
||||||
deathTimeStamp,
|
|
||||||
path: sessionFolder
|
|
||||||
}
|
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
|
||||||
await createFile(headersPath, 'Content-type: text/plain')
|
|
||||||
|
|
||||||
this.sessions.push(session)
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSessionController = (
|
export const getSessionController = (
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
): SASSessionController | JSSessionController | PythonSessionController => {
|
): SessionController => {
|
||||||
if (runTime === RunTimeType.SAS) {
|
if (process.sessionController) return process.sessionController
|
||||||
return getSASSessionController()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (runTime === RunTimeType.JS) {
|
process.sessionController =
|
||||||
return getJSSessionController()
|
runTime === RunTimeType.SAS
|
||||||
}
|
? new SASSessionController()
|
||||||
|
: new SessionController()
|
||||||
|
|
||||||
if (runTime === RunTimeType.PY) {
|
return process.sessionController
|
||||||
return getPythonSessionController()
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('No Runtime is configured')
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSASSessionController = (): SASSessionController => {
|
|
||||||
if (process.sasSessionController) return process.sasSessionController
|
|
||||||
|
|
||||||
process.sasSessionController = new SASSessionController()
|
|
||||||
|
|
||||||
return process.sasSessionController
|
|
||||||
}
|
|
||||||
|
|
||||||
const getJSSessionController = (): JSSessionController => {
|
|
||||||
if (process.jsSessionController) return process.jsSessionController
|
|
||||||
|
|
||||||
process.jsSessionController = new JSSessionController()
|
|
||||||
|
|
||||||
return process.jsSessionController
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPythonSessionController = (): PythonSessionController => {
|
|
||||||
if (process.pythonSessionController) return process.pythonSessionController
|
|
||||||
|
|
||||||
process.pythonSessionController = new PythonSessionController()
|
|
||||||
|
|
||||||
return process.pythonSessionController
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoExecContent = `
|
const autoExecContent = `
|
||||||
|
|||||||
68
api/src/controllers/internal/createRProgram.ts
Normal file
68
api/src/controllers/internal/createRProgram.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { isWindows } from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session } from '../../types'
|
||||||
|
import { generateFileUploadRCode } from '../../utils'
|
||||||
|
import { ExecutionVars } from '.'
|
||||||
|
|
||||||
|
export const createRProgram = async (
|
||||||
|
program: string,
|
||||||
|
preProgramVariables: PreProgramVars,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
session: Session,
|
||||||
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
|
tokenFile: string,
|
||||||
|
otherArgs?: any
|
||||||
|
) => {
|
||||||
|
const varStatments = Object.keys(vars).reduce(
|
||||||
|
(computed: string, key: string) => `${computed}.${key} <- '${vars[key]}'\n`,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
const preProgramVarStatments = `
|
||||||
|
._SASJS_SESSION_PATH <- '${
|
||||||
|
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
|
||||||
|
}';
|
||||||
|
._WEBOUT <- '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
|
||||||
|
._SASJS_WEBOUT_HEADERS <- '${headersPath}';
|
||||||
|
._SASJS_TOKENFILE <- '${
|
||||||
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
|
}';
|
||||||
|
._SASJS_USERNAME <- '${preProgramVariables?.username}';
|
||||||
|
._SASJS_USERID <- '${preProgramVariables?.userId}';
|
||||||
|
._SASJS_DISPLAYNAME <- '${preProgramVariables?.displayName}';
|
||||||
|
._METAPERSON <- ._SASJS_DISPLAYNAME;
|
||||||
|
._METAUSER <- ._SASJS_USERNAME;
|
||||||
|
SASJSPROCESSMODE <- 'Stored Program';
|
||||||
|
`
|
||||||
|
|
||||||
|
const requiredModules = ``
|
||||||
|
|
||||||
|
program = `
|
||||||
|
# runtime vars
|
||||||
|
${varStatments}
|
||||||
|
|
||||||
|
# dynamic user-provided vars
|
||||||
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
# change working directory to session folder
|
||||||
|
setwd(._SASJS_SESSION_PATH)
|
||||||
|
|
||||||
|
# actual job code
|
||||||
|
${program}
|
||||||
|
|
||||||
|
`
|
||||||
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
|
if (otherArgs?.filesNamesMap) {
|
||||||
|
const uploadRCode = await generateFileUploadRCode(
|
||||||
|
otherArgs.filesNamesMap,
|
||||||
|
session.path
|
||||||
|
)
|
||||||
|
|
||||||
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
|
if (uploadRCode.length > 0) {
|
||||||
|
program = `${uploadRCode}\n` + program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredModules + program
|
||||||
|
}
|
||||||
@@ -5,4 +5,5 @@ export * from './FileUploadController'
|
|||||||
export * from './createSASProgram'
|
export * from './createSASProgram'
|
||||||
export * from './createJSProgram'
|
export * from './createJSProgram'
|
||||||
export * from './createPythonProgram'
|
export * from './createPythonProgram'
|
||||||
|
export * from './createRProgram'
|
||||||
export * from './processProgram'
|
export * from './processProgram'
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
ExecutionVars,
|
ExecutionVars,
|
||||||
createSASProgram,
|
createSASProgram,
|
||||||
createJSProgram,
|
createJSProgram,
|
||||||
createPythonProgram
|
createPythonProgram,
|
||||||
|
createRProgram
|
||||||
} from './'
|
} from './'
|
||||||
|
|
||||||
export const processProgram = async (
|
export const processProgram = async (
|
||||||
@@ -24,81 +25,7 @@ export const processProgram = async (
|
|||||||
logPath: string,
|
logPath: string,
|
||||||
otherArgs?: any
|
otherArgs?: any
|
||||||
) => {
|
) => {
|
||||||
if (runTime === RunTimeType.JS) {
|
if (runTime === RunTimeType.SAS) {
|
||||||
program = await createJSProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
headersPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
|
|
||||||
const codePath = path.join(session.path, 'code.js')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createFile(codePath, program)
|
|
||||||
|
|
||||||
// create a stream that will write to console outputs to log file
|
|
||||||
const writeStream = fs.createWriteStream(logPath)
|
|
||||||
|
|
||||||
// waiting for the open event so that we can have underlying file descriptor
|
|
||||||
await once(writeStream, 'open')
|
|
||||||
|
|
||||||
execFileSync(process.nodeLoc!, [codePath], {
|
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
|
||||||
})
|
|
||||||
|
|
||||||
// copy the code.js program to log and end write stream
|
|
||||||
writeStream.end(program)
|
|
||||||
|
|
||||||
session.completed = true
|
|
||||||
console.log('session completed', session)
|
|
||||||
} catch (err: any) {
|
|
||||||
session.completed = true
|
|
||||||
session.crashed = err.toString()
|
|
||||||
console.log('session crashed', session.id, session.crashed)
|
|
||||||
}
|
|
||||||
} else if (runTime === RunTimeType.PY) {
|
|
||||||
program = await createPythonProgram(
|
|
||||||
program,
|
|
||||||
preProgramVariables,
|
|
||||||
vars,
|
|
||||||
session,
|
|
||||||
weboutPath,
|
|
||||||
headersPath,
|
|
||||||
tokenFile,
|
|
||||||
otherArgs
|
|
||||||
)
|
|
||||||
|
|
||||||
const codePath = path.join(session.path, 'code.py')
|
|
||||||
|
|
||||||
try {
|
|
||||||
await createFile(codePath, program)
|
|
||||||
|
|
||||||
// create a stream that will write to console outputs to log file
|
|
||||||
const writeStream = fs.createWriteStream(logPath)
|
|
||||||
|
|
||||||
// waiting for the open event so that we can have underlying file descriptor
|
|
||||||
await once(writeStream, 'open')
|
|
||||||
|
|
||||||
execFileSync(process.pythonLoc!, [codePath], {
|
|
||||||
stdio: ['ignore', writeStream, writeStream]
|
|
||||||
})
|
|
||||||
|
|
||||||
// copy the code.py program to log and end write stream
|
|
||||||
writeStream.end(program)
|
|
||||||
|
|
||||||
session.completed = true
|
|
||||||
console.log('session completed', session)
|
|
||||||
} catch (err: any) {
|
|
||||||
session.completed = true
|
|
||||||
session.crashed = err.toString()
|
|
||||||
console.log('session crashed', session.id, session.crashed)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
program = await createSASProgram(
|
program = await createSASProgram(
|
||||||
program,
|
program,
|
||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
@@ -124,6 +51,82 @@ export const processProgram = async (
|
|||||||
while (!session.completed) {
|
while (!session.completed) {
|
||||||
await delay(50)
|
await delay(50)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let codePath: string
|
||||||
|
let executablePath: string
|
||||||
|
switch (runTime) {
|
||||||
|
case RunTimeType.JS:
|
||||||
|
program = await createJSProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.js')
|
||||||
|
executablePath = process.nodeLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
case RunTimeType.PY:
|
||||||
|
program = await createPythonProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.py')
|
||||||
|
executablePath = process.pythonLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
case RunTimeType.R:
|
||||||
|
program = await createRProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
codePath = path.join(session.path, 'code.r')
|
||||||
|
executablePath = process.rLoc!
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('Invalid runtime!')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createFile(codePath, program)
|
||||||
|
|
||||||
|
// create a stream that will write to console outputs to log file
|
||||||
|
const writeStream = fs.createWriteStream(logPath)
|
||||||
|
|
||||||
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
|
execFileSync(executablePath, [codePath], {
|
||||||
|
stdio: ['ignore', writeStream, writeStream]
|
||||||
|
})
|
||||||
|
|
||||||
|
// copy the code file to log and end write stream
|
||||||
|
writeStream.end(program)
|
||||||
|
|
||||||
|
session.completed = true
|
||||||
|
console.log('session completed', session)
|
||||||
|
} catch (err: any) {
|
||||||
|
session.completed = true
|
||||||
|
session.crashed = err.toString()
|
||||||
|
console.log('session crashed', session.id, session.crashed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
191
api/src/controllers/mock-sas9.ts
Normal file
191
api/src/controllers/mock-sas9.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { readFile } from '@sasjs/utils'
|
||||||
|
import express from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import { Request, Post, Get } from 'tsoa'
|
||||||
|
|
||||||
|
export interface Sas9Response {
|
||||||
|
content: string
|
||||||
|
redirect?: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockFileRead {
|
||||||
|
content: string
|
||||||
|
error?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MockSas9Controller {
|
||||||
|
private loggedIn: string | undefined
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess')
|
||||||
|
public async sasStoredProcess(): Promise<Sas9Response> {
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'sas-stored-process'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASStoredProcess/do/')
|
||||||
|
public async sasStoredProcessDo(
|
||||||
|
@Request() req: express.Request
|
||||||
|
): Promise<Sas9Response> {
|
||||||
|
if (!this.loggedIn) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isPublicAccount()) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/Login'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let program = req.query._program?.toString() || ''
|
||||||
|
program = program.replace('/', '')
|
||||||
|
|
||||||
|
const content = await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
...program.split('/')
|
||||||
|
])
|
||||||
|
|
||||||
|
if (content.error) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedContent = parseJsonIfValid(content.content)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: parsedContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASLogon/login')
|
||||||
|
public async loginGet(): Promise<Sas9Response> {
|
||||||
|
if (this.loggedIn) {
|
||||||
|
if (this.isPublicAccount()) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASStoredProcess/Logoff?publicDenied=true'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'logged-in'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'login'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/SASLogon/login')
|
||||||
|
public async loginPost(req: express.Request): Promise<Sas9Response> {
|
||||||
|
this.loggedIn = req.body.username
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'logged-in'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASLogon/logout')
|
||||||
|
public async logout(req: express.Request): Promise<Sas9Response> {
|
||||||
|
this.loggedIn = undefined
|
||||||
|
|
||||||
|
if (req.query.publicDenied === 'true') {
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'public-access-denied'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getMockResponseFromFile([
|
||||||
|
process.cwd(),
|
||||||
|
'mocks',
|
||||||
|
'generic',
|
||||||
|
'sas9',
|
||||||
|
'logged-out'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/SASStoredProcess/Logoff') //publicDenied=true
|
||||||
|
public async logoff(req: express.Request): Promise<Sas9Response> {
|
||||||
|
const params = req.query.publicDenied
|
||||||
|
? `?publicDenied=${req.query.publicDenied}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
redirect: '/SASLogon/logout' + params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
filePath: string[]
|
||||||
|
): Promise<MockFileRead> => {
|
||||||
|
const filePathParsed = path.join(...filePath)
|
||||||
|
let error: boolean = false
|
||||||
|
|
||||||
|
let file = await readFile(filePathParsed).catch((err: any) => {
|
||||||
|
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
|
||||||
|
console.error(errMsg)
|
||||||
|
|
||||||
|
error = true
|
||||||
|
|
||||||
|
return errMsg
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: file,
|
||||||
|
error: error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,16 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import {
|
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
||||||
Request,
|
import { ExecutionController, ExecutionVars } from './internal'
|
||||||
Security,
|
|
||||||
Route,
|
|
||||||
Tags,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Get,
|
|
||||||
Query,
|
|
||||||
Example
|
|
||||||
} from 'tsoa'
|
|
||||||
import {
|
|
||||||
ExecuteReturnJson,
|
|
||||||
ExecuteReturnRaw,
|
|
||||||
ExecutionController,
|
|
||||||
ExecutionVars
|
|
||||||
} from './internal'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
|
||||||
LogLine,
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
parseLogToArray,
|
|
||||||
getRunTimeAndFilePath
|
getRunTimeAndFilePath
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecutePostRequestPayload {
|
||||||
/**
|
/**
|
||||||
* Location of SAS program
|
* Location of SAS program
|
||||||
* @example "/Public/somefolder/some.file"
|
* @example "/Public/somefolder/some.file"
|
||||||
@@ -35,17 +18,6 @@ interface ExecuteReturnJsonPayload {
|
|||||||
_program?: string
|
_program?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRecordOfAny {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
export interface ExecuteReturnJsonResponse {
|
|
||||||
status: string
|
|
||||||
_webout: string | IRecordOfAny
|
|
||||||
log: LogLine[]
|
|
||||||
message?: string
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/stp')
|
@Route('SASjsApi/stp')
|
||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
@@ -62,11 +34,12 @@ export class STPController {
|
|||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Get('/execute')
|
@Get('/execute')
|
||||||
public async executeReturnRaw(
|
public async executeGetRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Query() _program: string
|
@Query() _program: string
|
||||||
): Promise<string | Buffer> {
|
): Promise<string | Buffer> {
|
||||||
return executeReturnRaw(request, _program)
|
const vars = request.query as ExecutionVars
|
||||||
|
return execute(request, _program, vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,50 +60,42 @@ export class STPController {
|
|||||||
* @param _program Location of SAS or JS code
|
* @param _program Location of SAS or JS code
|
||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Example<ExecuteReturnJsonResponse>({
|
|
||||||
status: 'success',
|
|
||||||
_webout: 'webout content',
|
|
||||||
log: [],
|
|
||||||
httpHeaders: {
|
|
||||||
'Content-type': 'application/zip',
|
|
||||||
'Cache-Control': 'public, max-age=1000'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executeReturnJson(
|
public async executePostRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body?: ExecuteReturnJsonPayload,
|
@Body() body?: ExecutePostRequestPayload,
|
||||||
@Query() _program?: string
|
@Query() _program?: string
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
const program = _program ?? body?._program
|
const program = _program ?? body?._program
|
||||||
return executeReturnJson(request, program!)
|
const vars = { ...request.query, ...request.body }
|
||||||
|
const filesNamesMap = request.files?.length
|
||||||
|
? makeFilesNamesMap(request.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||||
|
|
||||||
|
return execute(request, program!, vars, otherArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnRaw = async (
|
const execute = async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string
|
_program: string,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
otherArgs?: any
|
||||||
): Promise<string | Buffer> => {
|
): Promise<string | Buffer> => {
|
||||||
const query = req.query as ExecutionVars
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
||||||
|
|
||||||
const { result, httpHeaders } =
|
const { result, httpHeaders } = await new ExecutionController().executeFile(
|
||||||
(await new ExecutionController().executeFile({
|
{
|
||||||
programPath: codePath,
|
programPath: codePath,
|
||||||
|
runTime,
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
vars: query,
|
vars,
|
||||||
runTime
|
otherArgs,
|
||||||
})) as ExecuteReturnRaw
|
session: req.sasjsSession
|
||||||
|
}
|
||||||
// Should over-ride response header for debug
|
)
|
||||||
// on GET request to see entire log rendering on browser.
|
|
||||||
if (isDebugOn(query)) {
|
|
||||||
httpHeaders['content-type'] = 'text/plain'
|
|
||||||
}
|
|
||||||
|
|
||||||
req.res?.set(httpHeaders)
|
|
||||||
|
|
||||||
if (result instanceof Buffer) {
|
if (result instanceof Buffer) {
|
||||||
;(req as any).sasHeaders = httpHeaders
|
;(req as any).sasHeaders = httpHeaders
|
||||||
@@ -146,48 +111,3 @@ const executeReturnRaw = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnJson = async (
|
|
||||||
req: express.Request,
|
|
||||||
_program: string
|
|
||||||
): Promise<ExecuteReturnJsonResponse> => {
|
|
||||||
const filesNamesMap = req.files?.length
|
|
||||||
? makeFilesNamesMap(req.files as MulterFile[])
|
|
||||||
: null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
|
||||||
|
|
||||||
const { webout, log, httpHeaders } =
|
|
||||||
(await new ExecutionController().executeFile({
|
|
||||||
programPath: codePath,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars: { ...req.query, ...req.body },
|
|
||||||
otherArgs: { filesNamesMap: filesNamesMap },
|
|
||||||
returnJson: true,
|
|
||||||
session: req.sasjsSession,
|
|
||||||
runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
let weboutRes: string | IRecordOfAny = webout
|
|
||||||
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
|
||||||
try {
|
|
||||||
weboutRes = JSON.parse(webout as string)
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
_webout: weboutRes,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'Job execution failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ import {
|
|||||||
} from '../../../utils'
|
} from '../../../utils'
|
||||||
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
||||||
import {
|
import {
|
||||||
SASSessionController,
|
SessionController,
|
||||||
JSSessionController,
|
SASSessionController
|
||||||
PythonSessionController
|
|
||||||
} from '../../../controllers/internal'
|
} from '../../../controllers/internal'
|
||||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
||||||
import { Session } from '../../../types'
|
import { Session } from '../../../types'
|
||||||
@@ -472,11 +471,7 @@ const setupMocks = async () => {
|
|||||||
.mockImplementation(mockedGetSession)
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(JSSessionController.prototype, 'getSession')
|
.spyOn(SASSessionController.prototype, 'getSession')
|
||||||
.mockImplementation(mockedGetSession)
|
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(PythonSessionController.prototype, 'getSession')
|
|
||||||
.mockImplementation(mockedGetSession)
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnRaw(req, query._program)
|
const response = await controller.executeGetRequest(req, query._program)
|
||||||
|
|
||||||
if (response instanceof Buffer) {
|
if (response instanceof Buffer) {
|
||||||
res.writeHead(200, (req as any).sasHeaders)
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
@@ -42,7 +42,7 @@ stpRouter.post(
|
|||||||
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnJson(
|
const response = await controller.executePostRequest(
|
||||||
req,
|
req,
|
||||||
req.body,
|
req.body,
|
||||||
req.query?._program as string
|
req.query?._program as string
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import sas9WebRouter from './sas9-web'
|
||||||
|
import sasViyaWebRouter from './sasviya-web'
|
||||||
import webRouter from './web'
|
import webRouter from './web'
|
||||||
|
import { MOCK_SERVERTYPEType } from '../../utils'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.use('/', webRouter)
|
const { MOCK_SERVERTYPE } = process.env
|
||||||
|
|
||||||
|
switch (MOCK_SERVERTYPE) {
|
||||||
|
case MOCK_SERVERTYPEType.SAS9: {
|
||||||
|
router.use('/', sas9WebRouter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case MOCK_SERVERTYPEType.SASVIYA: {
|
||||||
|
router.use('/', sasViyaWebRouter)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
router.use('/', webRouter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
118
api/src/routes/web/sas9-web.ts
Normal file
118
api/src/routes/web/sas9-web.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { WebController } from '../../controllers'
|
||||||
|
import { MockSas9Controller } from '../../controllers/mock-sas9'
|
||||||
|
|
||||||
|
const sas9WebRouter = express.Router()
|
||||||
|
const webController = new WebController()
|
||||||
|
// Mock controller must be singleton because it keeps the states
|
||||||
|
// for example `isLoggedIn` and potentially more in future mocks
|
||||||
|
const controller = new MockSas9Controller()
|
||||||
|
|
||||||
|
sas9WebRouter.get('/', async (req, res) => {
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await webController.home()
|
||||||
|
} catch (_) {
|
||||||
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
|
} finally {
|
||||||
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.send(injectedContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcess()
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
|
||||||
|
const response = await controller.sasStoredProcessDo(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) => {
|
||||||
|
const response = await controller.loginGet()
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
|
||||||
|
const response = await controller.loginPost(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/logout', async (req, res) => {
|
||||||
|
const response = await controller.logout(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('/SASStoredProcess/Logoff', async (req, res) => {
|
||||||
|
const response = await controller.logoff(req)
|
||||||
|
|
||||||
|
if (response.redirect) {
|
||||||
|
res.redirect(response.redirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.send(response.content)
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default sas9WebRouter
|
||||||
32
api/src/routes/web/sasviya-web.ts
Normal file
32
api/src/routes/web/sasviya-web.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { WebController } from '../../controllers/web'
|
||||||
|
|
||||||
|
const sasViyaWebRouter = express.Router()
|
||||||
|
const controller = new WebController()
|
||||||
|
|
||||||
|
sasViyaWebRouter.get('/', async (req, res) => {
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await controller.home()
|
||||||
|
} catch (_) {
|
||||||
|
response = '<html><head></head><body>Web Build is not present</body></html>'
|
||||||
|
} finally {
|
||||||
|
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
|
||||||
|
const injectedContent = response?.replace(
|
||||||
|
'</head>',
|
||||||
|
`${codeToInject}</head>`
|
||||||
|
)
|
||||||
|
|
||||||
|
return res.send(injectedContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sasViyaWebRouter.post('/SASJobExecution/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.send({ test: 'test' })
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(403).send(err.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default sasViyaWebRouter
|
||||||
6
api/src/types/system/process.d.ts
vendored
6
api/src/types/system/process.d.ts
vendored
@@ -3,11 +3,11 @@ declare namespace NodeJS {
|
|||||||
sasLoc?: string
|
sasLoc?: string
|
||||||
nodeLoc?: string
|
nodeLoc?: string
|
||||||
pythonLoc?: string
|
pythonLoc?: string
|
||||||
|
rLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
logsLoc: string
|
logsLoc: string
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
logsUUID: string
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
sessionController?: import('../../controllers/internal').SessionController
|
||||||
pythonSessionController?: import('../../controllers/internal').PythonSessionController
|
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
|
|||||||
import { RunTimeType } from './verifyEnvVariables'
|
import { RunTimeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, NODE_PATH, PYTHON_PATH } = process.env
|
const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env
|
||||||
|
|
||||||
let sasLoc, nodeLoc, pythonLoc
|
let sasLoc, nodeLoc, pythonLoc, rLoc
|
||||||
|
|
||||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
@@ -16,11 +16,15 @@ export const getDesktopFields = async () => {
|
|||||||
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.runTimes.includes(RunTimeType.JS)) {
|
if (process.runTimes.includes(RunTimeType.PY)) {
|
||||||
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
|
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sasLoc, nodeLoc, pythonLoc }
|
if (process.runTimes.includes(RunTimeType.R)) {
|
||||||
|
rLoc = R_PATH ?? (await getRLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sasLoc, nodeLoc, pythonLoc, rLoc }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriveLocation = async (): Promise<string> => {
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
@@ -117,3 +121,25 @@ const getPythonLocation = async (): Promise<string> => {
|
|||||||
|
|
||||||
return targetName
|
return targetName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to R executable is required.'
|
||||||
|
|
||||||
|
if (!(await fileExists(filePath))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows() ? 'C:\\Rscript' : '/usr/bin/Rscript'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter full path to a R executable: ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RunTimeType } from '.'
|
|||||||
|
|
||||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||||
const ext = path.extname(programPath)
|
const ext = path.extname(programPath)
|
||||||
// If programPath (_program) is provided with a ".sas", ".js" or ".py" extension
|
// If programPath (_program) is provided with a ".sas", ".js", ".py" or ".r" extension
|
||||||
// we should use that extension to determine the appropriate runTime
|
// we should use that extension to determine the appropriate runTime
|
||||||
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
||||||
const runTime = ext.slice(1)
|
const runTime = ext.slice(1)
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ export const setProcessVariables = async () => {
|
|||||||
process.sasLoc = process.env.SAS_PATH
|
process.sasLoc = process.env.SAS_PATH
|
||||||
process.nodeLoc = process.env.NODE_PATH
|
process.nodeLoc = process.env.NODE_PATH
|
||||||
process.pythonLoc = process.env.PYTHON_PATH
|
process.pythonLoc = process.env.PYTHON_PATH
|
||||||
|
process.rLoc = process.env.R_PATH
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc, pythonLoc } = 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
|
||||||
|
process.rLoc = rLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SASJS_ROOT } = process.env
|
const { SASJS_ROOT } = process.env
|
||||||
@@ -50,6 +52,8 @@ export const setProcessVariables = async () => {
|
|||||||
await createFolder(absLogsPath)
|
await createFolder(absLogsPath)
|
||||||
process.logsLoc = getRealPath(absLogsPath)
|
process.logsLoc = getRealPath(absLogsPath)
|
||||||
|
|
||||||
|
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
console.log('sasLogs: ', process.logsLoc)
|
console.log('sasLogs: ', process.logsLoc)
|
||||||
|
|||||||
@@ -157,3 +157,30 @@ export const generateFileUploadPythonCode = async (
|
|||||||
|
|
||||||
return uploadCode
|
return uploadCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the R code that references uploaded files in the concurrent request
|
||||||
|
* @param filesNamesMap object that maps hashed file names and original file names
|
||||||
|
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
|
||||||
|
* @returns generated python code
|
||||||
|
*/
|
||||||
|
export const generateFileUploadRCode = async (
|
||||||
|
filesNamesMap: FilenamesMap,
|
||||||
|
sessionFolder: string
|
||||||
|
) => {
|
||||||
|
let uploadCode = ''
|
||||||
|
let fileCount = 0
|
||||||
|
|
||||||
|
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
|
||||||
|
sessionFolderList.forEach(async (fileName) => {
|
||||||
|
if (fileName.includes('req_file')) {
|
||||||
|
fileCount++
|
||||||
|
uploadCode += `\n._WEBIN_FILENAME${fileCount} <- '${filesNamesMap[fileName].originalName}'`
|
||||||
|
uploadCode += `\n._WEBIN_NAME${fileCount} <- '${filesNamesMap[fileName].fieldName}'`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadCode += `\n._WEBIN_FILE_COUNT <- ${fileCount}`
|
||||||
|
|
||||||
|
return uploadCode
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
export enum MOCK_SERVERTYPEType {
|
||||||
|
SAS9 = 'sas9',
|
||||||
|
SASVIYA = 'sasviya'
|
||||||
|
}
|
||||||
|
|
||||||
export enum ModeType {
|
export enum ModeType {
|
||||||
Server = 'server',
|
Server = 'server',
|
||||||
Desktop = 'desktop'
|
Desktop = 'desktop'
|
||||||
@@ -29,7 +34,8 @@ export enum LOG_FORMAT_MORGANType {
|
|||||||
export enum RunTimeType {
|
export enum RunTimeType {
|
||||||
SAS = 'sas',
|
SAS = 'sas',
|
||||||
JS = 'js',
|
JS = 'js',
|
||||||
PY = 'py'
|
PY = 'py',
|
||||||
|
R = 'r'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReturnCode {
|
export enum ReturnCode {
|
||||||
@@ -40,6 +46,8 @@ export enum ReturnCode {
|
|||||||
export const verifyEnvVariables = (): ReturnCode => {
|
export const verifyEnvVariables = (): ReturnCode => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
|
|
||||||
|
errors.push(...verifyMOCK_SERVERTYPE())
|
||||||
|
|
||||||
errors.push(...verifyMODE())
|
errors.push(...verifyMODE())
|
||||||
|
|
||||||
errors.push(...verifyPROTOCOL())
|
errors.push(...verifyPROTOCOL())
|
||||||
@@ -66,6 +74,23 @@ export const verifyEnvVariables = (): ReturnCode => {
|
|||||||
return ReturnCode.Success
|
return ReturnCode.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verifyMOCK_SERVERTYPE = (): string[] => {
|
||||||
|
const errors: string[] = []
|
||||||
|
const { MOCK_SERVERTYPE } = process.env
|
||||||
|
|
||||||
|
if (MOCK_SERVERTYPE) {
|
||||||
|
const modeTypes = Object.values(MOCK_SERVERTYPEType)
|
||||||
|
if (!modeTypes.includes(MOCK_SERVERTYPE as MOCK_SERVERTYPEType))
|
||||||
|
errors.push(
|
||||||
|
`- MOCK_SERVERTYPE '${MOCK_SERVERTYPE}'\n - valid options ${modeTypes}`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
process.env.MOCK_SERVERTYPE = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
const verifyMODE = (): string[] => {
|
const verifyMODE = (): string[] => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const { MODE } = process.env
|
const { MODE } = process.env
|
||||||
@@ -229,7 +254,8 @@ const verifyRUN_TIMES = (): string[] => {
|
|||||||
|
|
||||||
const verifyExecutablePaths = () => {
|
const verifyExecutablePaths = () => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, MODE } = process.env
|
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } =
|
||||||
|
process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
const runTimes = RUN_TIMES?.split(',')
|
const runTimes = RUN_TIMES?.split(',')
|
||||||
@@ -245,6 +271,10 @@ const verifyExecutablePaths = () => {
|
|||||||
if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) {
|
if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) {
|
||||||
errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`)
|
errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runTimes?.includes(RunTimeType.R) && !R_PATH) {
|
||||||
|
errors.push(`- R_PATH is required for ${RunTimeType.R} run time`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const FilePathInputModal = ({
|
|||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/
|
const specialChars = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>?~]/
|
||||||
const fileExtension = /\.(exe|sh|htaccess)$/i
|
const fileExtension = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
if (specialChars.test(value)) {
|
if (specialChars.test(value)) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const NameInputModal = ({
|
|||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
||||||
const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/
|
const fileNameRegex = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>/?~]/
|
||||||
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ const TreeViewNode = ({
|
|||||||
mouseY: number
|
mouseY: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
const launchProgram = () => {
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const launchProgramWithDebug = () => {
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
window.open(
|
||||||
|
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handleContextMenu = (event: React.MouseEvent) => {
|
const handleContextMenu = (event: React.MouseEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -224,8 +236,8 @@ const TreeViewNode = ({
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{node.isFolder && (
|
{node.isFolder ? (
|
||||||
<div>
|
<>
|
||||||
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
|
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!node.relativePath}
|
disabled={!node.relativePath}
|
||||||
@@ -233,14 +245,21 @@ const TreeViewNode = ({
|
|||||||
>
|
>
|
||||||
Add File
|
Add File
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={launchProgram}>Launch</MenuItem>
|
||||||
|
<MenuItem onClick={launchProgramWithDebug}>
|
||||||
|
Launch and Debug
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!!node.relativePath && (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={handleRenameItemClick}>Rename</MenuItem>
|
||||||
|
<MenuItem onClick={handleDeleteItemClick}>Delete</MenuItem>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem disabled={!node.relativePath} onClick={handleRenameItemClick}>
|
|
||||||
Rename
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
|
||||||
Delete
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Permission from './permission'
|
|||||||
import Profile from './profile'
|
import Profile from './profile'
|
||||||
|
|
||||||
import { AppContext, ModeType } from '../../context/appContext'
|
import { AppContext, ModeType } from '../../context/appContext'
|
||||||
|
import PermissionsContextProvider from '../../context/permissionsContext'
|
||||||
|
|
||||||
const StyledTab = styled(Tab)({
|
const StyledTab = styled(Tab)({
|
||||||
background: 'black',
|
background: 'black',
|
||||||
@@ -64,7 +65,9 @@ const Settings = () => {
|
|||||||
<Profile />
|
<Profile />
|
||||||
</StyledTabpanel>
|
</StyledTabpanel>
|
||||||
<StyledTabpanel value="permission">
|
<StyledTabpanel value="permission">
|
||||||
<Permission />
|
<PermissionsContextProvider>
|
||||||
|
<Permission />
|
||||||
|
</PermissionsContextProvider>
|
||||||
</StyledTabpanel>
|
</StyledTabpanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { IconButton, Tooltip } from '@mui/material'
|
||||||
|
import { Add } from '@mui/icons-material'
|
||||||
|
import { RegisterPermissionPayload } from '../../../../utils/types'
|
||||||
|
import AddPermissionModal from './addPermissionModal'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
openModal: boolean
|
||||||
|
setOpenModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
addPermission: (
|
||||||
|
permissionsToAdd: RegisterPermissionPayload[],
|
||||||
|
permissionType: string,
|
||||||
|
principalType: string,
|
||||||
|
principal: string,
|
||||||
|
permissionSetting: string
|
||||||
|
) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddPermission = ({ openModal, setOpenModal, addPermission }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
sx={{ marginLeft: 'auto' }}
|
||||||
|
title="Add Permission"
|
||||||
|
placement="bottom-end"
|
||||||
|
>
|
||||||
|
<IconButton onClick={() => setOpenModal(true)}>
|
||||||
|
<Add />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<AddPermissionModal
|
||||||
|
open={openModal}
|
||||||
|
handleOpen={setOpenModal}
|
||||||
|
addPermission={addPermission}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddPermission
|
||||||
@@ -3,31 +3,21 @@ import axios from 'axios'
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
TextField,
|
TextField,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Autocomplete
|
Autocomplete
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
|
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialog } from '../../../../components/modal'
|
||||||
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UserResponse,
|
UserResponse,
|
||||||
GroupResponse,
|
GroupResponse,
|
||||||
RegisterPermissionPayload
|
RegisterPermissionPayload
|
||||||
} from '../../utils/types'
|
} from '../../../../utils/types'
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type AddPermissionModalProps = {
|
type AddPermissionModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Typography, Popover } from '@mui/material'
|
||||||
|
import { GroupDetailsResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
|
type DisplayGroupProps = {
|
||||||
|
group: GroupDetailsResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePopoverClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = Boolean(anchorEl)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography
|
||||||
|
aria-owns={open ? 'mouse-over-popover' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
onMouseEnter={handlePopoverOpen}
|
||||||
|
onMouseLeave={handlePopoverClose}
|
||||||
|
>
|
||||||
|
{group.name}
|
||||||
|
</Typography>
|
||||||
|
<Popover
|
||||||
|
id="mouse-over-popover"
|
||||||
|
sx={{
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left'
|
||||||
|
}}
|
||||||
|
onClose={handlePopoverClose}
|
||||||
|
disableRestoreFocus
|
||||||
|
>
|
||||||
|
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
||||||
|
Group Members
|
||||||
|
</Typography>
|
||||||
|
{group.users.map((user, index) => (
|
||||||
|
<Typography key={index} sx={{ p: 1 }} component="li">
|
||||||
|
{user.username}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DisplayGroup
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { Dispatch, SetStateAction, useState } from 'react'
|
||||||
|
import { IconButton, Tooltip } from '@mui/material'
|
||||||
|
import { FilterList } from '@mui/icons-material'
|
||||||
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
import PermissionFilterModal from './permissionFilterModal'
|
||||||
|
import { PrincipalType } from '../hooks/usePermission'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
handleOpen: Dispatch<SetStateAction<boolean>>
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
applyFilter: (
|
||||||
|
pathFilter: string[],
|
||||||
|
principalFilter: string[],
|
||||||
|
principalTypeFilter: PrincipalType[],
|
||||||
|
settingFilter: string[]
|
||||||
|
) => void
|
||||||
|
resetFilter: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterPermissions = ({
|
||||||
|
open,
|
||||||
|
handleOpen,
|
||||||
|
permissions,
|
||||||
|
applyFilter,
|
||||||
|
resetFilter
|
||||||
|
}: Props) => {
|
||||||
|
const [pathFilter, setPathFilter] = useState<string[]>([])
|
||||||
|
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
||||||
|
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
||||||
|
PrincipalType[]
|
||||||
|
>([])
|
||||||
|
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
||||||
|
const handleApplyFilter = () => {
|
||||||
|
applyFilter(pathFilter, principalFilter, principalTypeFilter, settingFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetFilter = () => {
|
||||||
|
setPathFilter([])
|
||||||
|
setPrincipalFilter([])
|
||||||
|
setPrincipalFilter([])
|
||||||
|
setSettingFilter([])
|
||||||
|
resetFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Filter Permissions">
|
||||||
|
<IconButton onClick={() => handleOpen(true)}>
|
||||||
|
<FilterList />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<PermissionFilterModal
|
||||||
|
open={open}
|
||||||
|
handleOpen={handleOpen}
|
||||||
|
permissions={permissions}
|
||||||
|
pathFilter={pathFilter}
|
||||||
|
setPathFilter={setPathFilter}
|
||||||
|
principalFilter={principalFilter}
|
||||||
|
setPrincipalFilter={setPrincipalFilter}
|
||||||
|
principalTypeFilter={principalTypeFilter}
|
||||||
|
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
||||||
|
settingFilter={settingFilter}
|
||||||
|
setSettingFilter={setSettingFilter}
|
||||||
|
applyFilter={handleApplyFilter}
|
||||||
|
resetFilter={handleResetFilter}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterPermissions
|
||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
import Autocomplete from '@mui/material/Autocomplete'
|
import Autocomplete from '@mui/material/Autocomplete'
|
||||||
|
|
||||||
import { PermissionResponse } from '../../utils/types'
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
import { PrincipalType } from './permission'
|
import { PrincipalType } from '../hooks/usePermission'
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
||||||
'& .MuiDialogContent-root': {
|
'& .MuiDialogContent-root': {
|
||||||
@@ -2,9 +2,9 @@ import React from 'react'
|
|||||||
|
|
||||||
import { Typography, DialogContent } from '@mui/material'
|
import { Typography, DialogContent } from '@mui/material'
|
||||||
|
|
||||||
import { BootstrapDialog } from '../../components/modal'
|
import { BootstrapDialog } from '../../../../components/modal'
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
import { PermissionResponse } from '../../utils/types'
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
export interface PermissionResponsePayload {
|
export interface PermissionResponsePayload {
|
||||||
permissionType: string
|
permissionType: string
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
IconButton,
|
||||||
|
Tooltip
|
||||||
|
} from '@mui/material'
|
||||||
|
|
||||||
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
|
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
||||||
|
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
|
import { AppContext } from '../../../../context/appContext'
|
||||||
|
import { displayPrincipal, displayPrincipalType } from '../helper'
|
||||||
|
|
||||||
|
const BootstrapTableCell = styled(TableCell)({
|
||||||
|
textAlign: 'left'
|
||||||
|
})
|
||||||
|
|
||||||
|
export enum PrincipalType {
|
||||||
|
User = 'User',
|
||||||
|
Group = 'Group'
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermissionTableProps = {
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
||||||
|
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PermissionTable = ({
|
||||||
|
permissions,
|
||||||
|
handleUpdatePermissionClick,
|
||||||
|
handleDeletePermissionClick
|
||||||
|
}: PermissionTableProps) => {
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table sx={{ minWidth: 650 }}>
|
||||||
|
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
||||||
|
<TableRow>
|
||||||
|
<BootstrapTableCell>Path</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Principal</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>Setting</BootstrapTableCell>
|
||||||
|
{appContext.isAdmin && (
|
||||||
|
<BootstrapTableCell>Action</BootstrapTableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{permissions.map((permission) => (
|
||||||
|
<TableRow key={permission.permissionId}>
|
||||||
|
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>
|
||||||
|
{displayPrincipal(permission)}
|
||||||
|
</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>
|
||||||
|
{displayPrincipalType(permission)}
|
||||||
|
</BootstrapTableCell>
|
||||||
|
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
||||||
|
{appContext.isAdmin && (
|
||||||
|
<BootstrapTableCell>
|
||||||
|
<Tooltip title="Edit Permission">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => handleUpdatePermissionClick(permission)}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Delete Permission">
|
||||||
|
<IconButton
|
||||||
|
color="error"
|
||||||
|
onClick={() => handleDeletePermissionClick(permission)}
|
||||||
|
>
|
||||||
|
<DeleteForeverIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</BootstrapTableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionTable
|
||||||
@@ -2,26 +2,17 @@ import React, { useState, Dispatch, SetStateAction, useEffect } from 'react'
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
TextField
|
TextField
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { styled } from '@mui/material/styles'
|
|
||||||
import Autocomplete from '@mui/material/Autocomplete'
|
import Autocomplete from '@mui/material/Autocomplete'
|
||||||
|
|
||||||
import { BootstrapDialogTitle } from '../../components/dialogTitle'
|
import { BootstrapDialog } from '../../../../components/modal'
|
||||||
|
import { BootstrapDialogTitle } from '../../../../components/dialogTitle'
|
||||||
|
|
||||||
import { PermissionResponse } from '../../utils/types'
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
|
||||||
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
'& .MuiDialogContent-root': {
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
'& .MuiDialogActions-root': {
|
|
||||||
padding: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
type UpdatePermissionModalProps = {
|
type UpdatePermissionModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
13
web/src/containers/Settings/internal/helper.tsx
Normal file
13
web/src/containers/Settings/internal/helper.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { PermissionResponse } from '../../../utils/types'
|
||||||
|
import { PrincipalType } from './hooks/usePermission'
|
||||||
|
import DisplayGroup from './components/displayGroup'
|
||||||
|
|
||||||
|
export const displayPrincipal = (permission: PermissionResponse) => {
|
||||||
|
if (permission.user) return permission.user.username
|
||||||
|
if (permission.group) return <DisplayGroup group={permission.group} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayPrincipalType = (permission: PermissionResponse) => {
|
||||||
|
if (permission.user) return PrincipalType.User
|
||||||
|
if (permission.group) return PrincipalType.Group
|
||||||
|
}
|
||||||
109
web/src/containers/Settings/internal/hooks/useAddPermission.tsx
Normal file
109
web/src/containers/Settings/internal/hooks/useAddPermission.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import {
|
||||||
|
PermissionResponse,
|
||||||
|
RegisterPermissionPayload
|
||||||
|
} from '../../../../utils/types'
|
||||||
|
import AddPermission from '../components/addPermission'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import {
|
||||||
|
findExistingPermission,
|
||||||
|
findUpdatingPermission
|
||||||
|
} from '../../../../utils/helper'
|
||||||
|
|
||||||
|
const useAddPermission = () => {
|
||||||
|
const {
|
||||||
|
permissions,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setPermissionResponsePayload,
|
||||||
|
setOpenPermissionResponseModal
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
|
||||||
|
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const addPermission = async (
|
||||||
|
permissionsToAdd: RegisterPermissionPayload[],
|
||||||
|
permissionType: string,
|
||||||
|
principalType: string,
|
||||||
|
principal: string,
|
||||||
|
permissionSetting: string
|
||||||
|
) => {
|
||||||
|
setAddPermissionModalOpen(false)
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const newAddedPermissions: PermissionResponse[] = []
|
||||||
|
const updatedPermissions: PermissionResponse[] = []
|
||||||
|
const errorPaths: string[] = []
|
||||||
|
|
||||||
|
const existingPermissions: PermissionResponse[] = []
|
||||||
|
const updatingPermissions: PermissionResponse[] = []
|
||||||
|
const newPermissions: RegisterPermissionPayload[] = []
|
||||||
|
|
||||||
|
permissionsToAdd.forEach((permission) => {
|
||||||
|
const existingPermission = findExistingPermission(permissions, permission)
|
||||||
|
if (existingPermission) {
|
||||||
|
existingPermissions.push(existingPermission)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatingPermission = findUpdatingPermission(permissions, permission)
|
||||||
|
if (updatingPermission) {
|
||||||
|
updatingPermissions.push(updatingPermission)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newPermissions.push(permission)
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const permission of newPermissions) {
|
||||||
|
await axios
|
||||||
|
.post('/SASjsApi/permission', permission)
|
||||||
|
.then((res) => {
|
||||||
|
newAddedPermissions.push(res.data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorPaths.push(permission.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const permission of updatingPermissions) {
|
||||||
|
await axios
|
||||||
|
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
|
||||||
|
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
updatedPermissions.push(res.data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
errorPaths.push(permission.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPermissions()
|
||||||
|
setIsLoading(false)
|
||||||
|
setPermissionResponsePayload({
|
||||||
|
permissionType,
|
||||||
|
principalType,
|
||||||
|
principal,
|
||||||
|
permissionSetting,
|
||||||
|
existingPermissions,
|
||||||
|
updatedPermissions,
|
||||||
|
newAddedPermissions,
|
||||||
|
errorPaths
|
||||||
|
})
|
||||||
|
setOpenPermissionResponseModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddPermissionButton = () => (
|
||||||
|
<AddPermission
|
||||||
|
openModal={addPermissionModalOpen}
|
||||||
|
setOpenModal={setAddPermissionModalOpen}
|
||||||
|
addPermission={addPermission}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { AddPermissionButton, setAddPermissionModalOpen }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAddPermission
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||||
|
import DeleteConfirmationModal from '../../../../components/deleteConfirmationModal'
|
||||||
|
|
||||||
|
const useDeletePermissionModal = () => {
|
||||||
|
const {
|
||||||
|
selectedPermission,
|
||||||
|
setSelectedPermission,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setSnackbarMessage,
|
||||||
|
setSnackbarSeverity,
|
||||||
|
setOpenSnackbar,
|
||||||
|
setModalTitle,
|
||||||
|
setModalPayload,
|
||||||
|
setOpenModal
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const deletePermission = () => {
|
||||||
|
setDeleteConfirmationModalOpen(false)
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
||||||
|
.then((res: any) => {
|
||||||
|
fetchPermissions()
|
||||||
|
setSnackbarMessage('Permission deleted!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setSelectedPermission(undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeletePermissionDialog = () => (
|
||||||
|
<DeleteConfirmationModal
|
||||||
|
open={deleteConfirmationModalOpen}
|
||||||
|
setOpen={setDeleteConfirmationModalOpen}
|
||||||
|
message="Are you sure you want to delete this permission?"
|
||||||
|
_delete={deletePermission}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { DeletePermissionDialog, setDeleteConfirmationModalOpen }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDeletePermissionModal
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { PrincipalType } from './usePermission'
|
||||||
|
import FilterPermissions from '../components/filterPermissions'
|
||||||
|
|
||||||
|
const useFilterPermissions = () => {
|
||||||
|
const { permissions, setFilteredPermissions, setFilterApplied } =
|
||||||
|
useContext(PermissionsContext)
|
||||||
|
|
||||||
|
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* first find the permissions w.r.t each filter type
|
||||||
|
* take intersection of resultant arrays
|
||||||
|
*/
|
||||||
|
const applyFilter = (
|
||||||
|
pathFilter: string[],
|
||||||
|
principalFilter: string[],
|
||||||
|
principalTypeFilter: PrincipalType[],
|
||||||
|
settingFilter: string[]
|
||||||
|
) => {
|
||||||
|
setFilterModalOpen(false)
|
||||||
|
|
||||||
|
const uriFilteredPermissions =
|
||||||
|
pathFilter.length > 0
|
||||||
|
? permissions.filter((permission) =>
|
||||||
|
pathFilter.includes(permission.path)
|
||||||
|
)
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
const principalFilteredPermissions =
|
||||||
|
principalFilter.length > 0
|
||||||
|
? permissions.filter((permission) => {
|
||||||
|
if (permission.user) {
|
||||||
|
return principalFilter.includes(permission.user.username)
|
||||||
|
}
|
||||||
|
if (permission.group) {
|
||||||
|
return principalFilter.includes(permission.group.name)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
const principalTypeFilteredPermissions =
|
||||||
|
principalTypeFilter.length > 0
|
||||||
|
? permissions.filter((permission) => {
|
||||||
|
if (permission.user) {
|
||||||
|
return principalTypeFilter.includes(PrincipalType.User)
|
||||||
|
}
|
||||||
|
if (permission.group) {
|
||||||
|
return principalTypeFilter.includes(PrincipalType.Group)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
const settingFilteredPermissions =
|
||||||
|
settingFilter.length > 0
|
||||||
|
? permissions.filter((permission) =>
|
||||||
|
settingFilter.includes(permission.setting)
|
||||||
|
)
|
||||||
|
: permissions
|
||||||
|
|
||||||
|
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
||||||
|
principalFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
filteredArray = filteredArray.filter((permission) =>
|
||||||
|
principalTypeFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
filteredArray = filteredArray.filter((permission) =>
|
||||||
|
settingFilteredPermissions.some(
|
||||||
|
(item) => item.permissionId === permission.permissionId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
setFilteredPermissions(filteredArray)
|
||||||
|
setFilterApplied(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFilter = () => {
|
||||||
|
setFilterModalOpen(false)
|
||||||
|
setFilterApplied(false)
|
||||||
|
setFilteredPermissions([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterPermissionsButton = () => (
|
||||||
|
<FilterPermissions
|
||||||
|
open={filterModalOpen}
|
||||||
|
handleOpen={setFilterModalOpen}
|
||||||
|
permissions={permissions}
|
||||||
|
applyFilter={applyFilter}
|
||||||
|
resetFilter={resetFilter}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { FilterPermissionsButton }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useFilterPermissions
|
||||||
71
web/src/containers/Settings/internal/hooks/usePermission.ts
Normal file
71
web/src/containers/Settings/internal/hooks/usePermission.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useContext, useEffect } from 'react'
|
||||||
|
import { AppContext } from '../../../../context/appContext'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { PermissionResponse } from '../../../../utils/types'
|
||||||
|
import useAddPermission from './useAddPermission'
|
||||||
|
import useUpdatePermissionModal from './useUpdatePermissionModal'
|
||||||
|
import useDeletePermissionModal from './useDeletePermissionModal'
|
||||||
|
import useFilterPermissions from './useFilterPermissions'
|
||||||
|
|
||||||
|
export enum PrincipalType {
|
||||||
|
User = 'User',
|
||||||
|
Group = 'Group'
|
||||||
|
}
|
||||||
|
|
||||||
|
const usePermission = () => {
|
||||||
|
const { isAdmin } = useContext(AppContext)
|
||||||
|
const {
|
||||||
|
filterApplied,
|
||||||
|
filteredPermissions,
|
||||||
|
isLoading,
|
||||||
|
permissions,
|
||||||
|
Dialog,
|
||||||
|
Snackbar,
|
||||||
|
PermissionResponseDialog,
|
||||||
|
fetchPermissions,
|
||||||
|
setSelectedPermission
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
|
||||||
|
const { AddPermissionButton } = useAddPermission()
|
||||||
|
|
||||||
|
const { UpdatePermissionDialog, setUpdatePermissionModalOpen } =
|
||||||
|
useUpdatePermissionModal()
|
||||||
|
|
||||||
|
const { DeletePermissionDialog, setDeleteConfirmationModalOpen } =
|
||||||
|
useDeletePermissionModal()
|
||||||
|
|
||||||
|
const { FilterPermissionsButton } = useFilterPermissions()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fetchPermissions) fetchPermissions()
|
||||||
|
}, [fetchPermissions])
|
||||||
|
|
||||||
|
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
||||||
|
setSelectedPermission(permission)
|
||||||
|
setUpdatePermissionModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
||||||
|
setSelectedPermission(permission)
|
||||||
|
setDeleteConfirmationModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filterApplied,
|
||||||
|
filteredPermissions,
|
||||||
|
isAdmin,
|
||||||
|
isLoading,
|
||||||
|
permissions,
|
||||||
|
AddPermissionButton,
|
||||||
|
UpdatePermissionDialog,
|
||||||
|
DeletePermissionDialog,
|
||||||
|
FilterPermissionsButton,
|
||||||
|
handleDeletePermissionClick,
|
||||||
|
handleUpdatePermissionClick,
|
||||||
|
PermissionResponseDialog,
|
||||||
|
Dialog,
|
||||||
|
Snackbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePermission
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import PermissionResponseModal, {
|
||||||
|
PermissionResponsePayload
|
||||||
|
} from '../components/permissionResponseModal'
|
||||||
|
|
||||||
|
const usePermissionResponseModal = () => {
|
||||||
|
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
||||||
|
useState(false)
|
||||||
|
const [permissionResponsePayload, setPermissionResponsePayload] =
|
||||||
|
useState<PermissionResponsePayload>({
|
||||||
|
permissionType: '',
|
||||||
|
principalType: '',
|
||||||
|
principal: '',
|
||||||
|
permissionSetting: '',
|
||||||
|
existingPermissions: [],
|
||||||
|
newAddedPermissions: [],
|
||||||
|
updatedPermissions: [],
|
||||||
|
errorPaths: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const PermissionResponseDialog = () => (
|
||||||
|
<PermissionResponseModal
|
||||||
|
open={openPermissionResponseModal}
|
||||||
|
setOpen={setOpenPermissionResponseModal}
|
||||||
|
payload={permissionResponsePayload}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
PermissionResponseDialog,
|
||||||
|
setOpenPermissionResponseModal,
|
||||||
|
setPermissionResponsePayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePermissionResponseModal
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { useState, useContext } from 'react'
|
||||||
|
import UpdatePermissionModal from '../components/updatePermissionModal'
|
||||||
|
import { PermissionsContext } from '../../../../context/permissionsContext'
|
||||||
|
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||||
|
|
||||||
|
const useUpdatePermissionModal = () => {
|
||||||
|
const {
|
||||||
|
selectedPermission,
|
||||||
|
setSelectedPermission,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setSnackbarMessage,
|
||||||
|
setSnackbarSeverity,
|
||||||
|
setOpenSnackbar,
|
||||||
|
setModalTitle,
|
||||||
|
setModalPayload,
|
||||||
|
setOpenModal
|
||||||
|
} = useContext(PermissionsContext)
|
||||||
|
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const updatePermission = (setting: string) => {
|
||||||
|
setUpdatePermissionModalOpen(false)
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
||||||
|
setting
|
||||||
|
})
|
||||||
|
.then((res: any) => {
|
||||||
|
fetchPermissions()
|
||||||
|
setSnackbarMessage('Permission updated!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setSelectedPermission(undefined)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdatePermissionDialog = () => (
|
||||||
|
<UpdatePermissionModal
|
||||||
|
open={updatePermissionModalOpen}
|
||||||
|
handleOpen={setUpdatePermissionModalOpen}
|
||||||
|
permission={selectedPermission}
|
||||||
|
updatePermission={updatePermission}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { UpdatePermissionDialog, setUpdatePermissionModalOpen }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useUpdatePermissionModal
|
||||||
@@ -1,54 +1,7 @@
|
|||||||
import React, { useState, useEffect, useContext, useCallback } from 'react'
|
import { Box, Paper, Grid, CircularProgress } from '@mui/material'
|
||||||
import axios from 'axios'
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
Paper,
|
|
||||||
Grid,
|
|
||||||
CircularProgress,
|
|
||||||
IconButton,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
Popover
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
import FilterListIcon from '@mui/icons-material/FilterList'
|
|
||||||
import AddIcon from '@mui/icons-material/Add'
|
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
|
||||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
|
||||||
|
|
||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
|
import PermissionTable from './internal/components/permissionTable'
|
||||||
import Modal from '../../components/modal'
|
import usePermission from './internal/hooks/usePermission'
|
||||||
import PermissionFilterModal from './permissionFilterModal'
|
|
||||||
import AddPermissionModal from './addPermissionModal'
|
|
||||||
import PermissionResponseModal, {
|
|
||||||
PermissionResponsePayload
|
|
||||||
} from './addPermissionResponseModal'
|
|
||||||
import UpdatePermissionModal from './updatePermissionModal'
|
|
||||||
import DeleteConfirmationModal from '../../components/deleteConfirmationModal'
|
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
|
||||||
|
|
||||||
import {
|
|
||||||
GroupDetailsResponse,
|
|
||||||
PermissionResponse,
|
|
||||||
RegisterPermissionPayload
|
|
||||||
} from '../../utils/types'
|
|
||||||
import {
|
|
||||||
findExistingPermission,
|
|
||||||
findUpdatingPermission
|
|
||||||
} from '../../utils/helper'
|
|
||||||
|
|
||||||
import { AppContext } from '../../context/appContext'
|
|
||||||
|
|
||||||
const BootstrapTableCell = styled(TableCell)({
|
|
||||||
textAlign: 'left'
|
|
||||||
})
|
|
||||||
|
|
||||||
const BootstrapGridItem = styled(Grid)({
|
const BootstrapGridItem = styled(Grid)({
|
||||||
'&.MuiGrid-item': {
|
'&.MuiGrid-item': {
|
||||||
@@ -56,298 +9,23 @@ const BootstrapGridItem = styled(Grid)({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export enum PrincipalType {
|
|
||||||
User = 'User',
|
|
||||||
Group = 'Group'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Permission = () => {
|
const Permission = () => {
|
||||||
const appContext = useContext(AppContext)
|
const {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
filterApplied,
|
||||||
const [openModal, setOpenModal] = useState(false)
|
filteredPermissions,
|
||||||
const [modalTitle, setModalTitle] = useState('')
|
isAdmin,
|
||||||
const [modalPayload, setModalPayload] = useState('')
|
isLoading,
|
||||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
permissions,
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
AddPermissionButton,
|
||||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
UpdatePermissionDialog,
|
||||||
AlertSeverityType.Success
|
DeletePermissionDialog,
|
||||||
)
|
FilterPermissionsButton,
|
||||||
const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false)
|
handleDeletePermissionClick,
|
||||||
const [openPermissionResponseModal, setOpenPermissionResponseModal] =
|
handleUpdatePermissionClick,
|
||||||
useState(false)
|
PermissionResponseDialog,
|
||||||
const [permissionResponsePayload, setPermissionResponsePayload] =
|
Dialog,
|
||||||
useState<PermissionResponsePayload>({
|
Snackbar
|
||||||
permissionType: '',
|
} = usePermission()
|
||||||
principalType: '',
|
|
||||||
principal: '',
|
|
||||||
permissionSetting: '',
|
|
||||||
existingPermissions: [],
|
|
||||||
newAddedPermissions: [],
|
|
||||||
updatedPermissions: [],
|
|
||||||
errorPaths: []
|
|
||||||
})
|
|
||||||
|
|
||||||
const [updatePermissionModalOpen, setUpdatePermissionModalOpen] =
|
|
||||||
useState(false)
|
|
||||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
|
||||||
useState(false)
|
|
||||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
|
||||||
useState('')
|
|
||||||
const [selectedPermission, setSelectedPermission] =
|
|
||||||
useState<PermissionResponse>()
|
|
||||||
const [filterModalOpen, setFilterModalOpen] = useState(false)
|
|
||||||
const [pathFilter, setPathFilter] = useState<string[]>([])
|
|
||||||
const [principalFilter, setPrincipalFilter] = useState<string[]>([])
|
|
||||||
const [principalTypeFilter, setPrincipalTypeFilter] = useState<
|
|
||||||
PrincipalType[]
|
|
||||||
>([])
|
|
||||||
const [settingFilter, setSettingFilter] = useState<string[]>([])
|
|
||||||
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
|
||||||
const [filteredPermissions, setFilteredPermissions] = useState<
|
|
||||||
PermissionResponse[]
|
|
||||||
>([])
|
|
||||||
const [filterApplied, setFilterApplied] = useState(false)
|
|
||||||
|
|
||||||
const fetchPermissions = useCallback(() => {
|
|
||||||
axios
|
|
||||||
.get(`/SASjsApi/permission`)
|
|
||||||
.then((res: any) => {
|
|
||||||
if (res.data?.length > 0) {
|
|
||||||
setPermissions(res.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPermissions()
|
|
||||||
}, [fetchPermissions])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* first find the permissions w.r.t each filter type
|
|
||||||
* take intersection of resultant arrays
|
|
||||||
*/
|
|
||||||
const applyFilter = () => {
|
|
||||||
setFilterModalOpen(false)
|
|
||||||
|
|
||||||
const uriFilteredPermissions =
|
|
||||||
pathFilter.length > 0
|
|
||||||
? permissions.filter((permission) =>
|
|
||||||
pathFilter.includes(permission.path)
|
|
||||||
)
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const principalFilteredPermissions =
|
|
||||||
principalFilter.length > 0
|
|
||||||
? permissions.filter((permission) => {
|
|
||||||
if (permission.user) {
|
|
||||||
return principalFilter.includes(permission.user.username)
|
|
||||||
}
|
|
||||||
if (permission.group) {
|
|
||||||
return principalFilter.includes(permission.group.name)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const principalTypeFilteredPermissions =
|
|
||||||
principalTypeFilter.length > 0
|
|
||||||
? permissions.filter((permission) => {
|
|
||||||
if (permission.user) {
|
|
||||||
return principalTypeFilter.includes(PrincipalType.User)
|
|
||||||
}
|
|
||||||
if (permission.group) {
|
|
||||||
return principalTypeFilter.includes(PrincipalType.Group)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
const settingFilteredPermissions =
|
|
||||||
settingFilter.length > 0
|
|
||||||
? permissions.filter((permission) =>
|
|
||||||
settingFilter.includes(permission.setting)
|
|
||||||
)
|
|
||||||
: permissions
|
|
||||||
|
|
||||||
let filteredArray = uriFilteredPermissions.filter((permission) =>
|
|
||||||
principalFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
filteredArray = filteredArray.filter((permission) =>
|
|
||||||
principalTypeFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
filteredArray = filteredArray.filter((permission) =>
|
|
||||||
settingFilteredPermissions.some(
|
|
||||||
(item) => item.permissionId === permission.permissionId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
setFilteredPermissions(filteredArray)
|
|
||||||
setFilterApplied(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetFilter = () => {
|
|
||||||
setFilterModalOpen(false)
|
|
||||||
setPathFilter([])
|
|
||||||
setPrincipalFilter([])
|
|
||||||
setSettingFilter([])
|
|
||||||
setFilteredPermissions([])
|
|
||||||
setFilterApplied(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPermission = async (
|
|
||||||
permissionsToAdd: RegisterPermissionPayload[],
|
|
||||||
permissionType: string,
|
|
||||||
principalType: string,
|
|
||||||
principal: string,
|
|
||||||
permissionSetting: string
|
|
||||||
) => {
|
|
||||||
setAddPermissionModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
const newAddedPermissions: PermissionResponse[] = []
|
|
||||||
const updatedPermissions: PermissionResponse[] = []
|
|
||||||
const errorPaths: string[] = []
|
|
||||||
|
|
||||||
const existingPermissions: PermissionResponse[] = []
|
|
||||||
const updatingPermissions: PermissionResponse[] = []
|
|
||||||
const newPermissions: RegisterPermissionPayload[] = []
|
|
||||||
|
|
||||||
permissionsToAdd.forEach((permission) => {
|
|
||||||
const existingPermission = findExistingPermission(permissions, permission)
|
|
||||||
if (existingPermission) {
|
|
||||||
existingPermissions.push(existingPermission)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatingPermission = findUpdatingPermission(permissions, permission)
|
|
||||||
if (updatingPermission) {
|
|
||||||
updatingPermissions.push(updatingPermission)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
newPermissions.push(permission)
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const permission of newPermissions) {
|
|
||||||
await axios
|
|
||||||
.post('/SASjsApi/permission', permission)
|
|
||||||
.then((res) => {
|
|
||||||
newAddedPermissions.push(res.data)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
errorPaths.push(permission.path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const permission of updatingPermissions) {
|
|
||||||
await axios
|
|
||||||
.patch(`/SASjsApi/permission/${permission.permissionId}`, {
|
|
||||||
setting: permission.setting === 'Grant' ? 'Deny' : 'Grant'
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
updatedPermissions.push(res.data)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
errorPaths.push(permission.path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchPermissions()
|
|
||||||
setIsLoading(false)
|
|
||||||
setPermissionResponsePayload({
|
|
||||||
permissionType,
|
|
||||||
principalType,
|
|
||||||
principal,
|
|
||||||
permissionSetting,
|
|
||||||
existingPermissions,
|
|
||||||
updatedPermissions,
|
|
||||||
newAddedPermissions,
|
|
||||||
errorPaths
|
|
||||||
})
|
|
||||||
setOpenPermissionResponseModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdatePermissionClick = (permission: PermissionResponse) => {
|
|
||||||
setSelectedPermission(permission)
|
|
||||||
setUpdatePermissionModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePermission = (setting: string) => {
|
|
||||||
setUpdatePermissionModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.patch(`/SASjsApi/permission/${selectedPermission?.permissionId}`, {
|
|
||||||
setting
|
|
||||||
})
|
|
||||||
.then((res: any) => {
|
|
||||||
fetchPermissions()
|
|
||||||
setSnackbarMessage('Permission updated!')
|
|
||||||
setSnackbarSeverity(AlertSeverityType.Success)
|
|
||||||
setOpenSnackbar(true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setSelectedPermission(undefined)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeletePermissionClick = (permission: PermissionResponse) => {
|
|
||||||
setSelectedPermission(permission)
|
|
||||||
setDeleteConfirmationModalOpen(true)
|
|
||||||
setDeleteConfirmationModalMessage(
|
|
||||||
'Are you sure you want to delete this permission?'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletePermission = () => {
|
|
||||||
setDeleteConfirmationModalOpen(false)
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.delete(`/SASjsApi/permission/${selectedPermission?.permissionId}`)
|
|
||||||
.then((res: any) => {
|
|
||||||
fetchPermissions()
|
|
||||||
setSnackbarMessage('Permission deleted!')
|
|
||||||
setSnackbarSeverity(AlertSeverityType.Success)
|
|
||||||
setOpenSnackbar(true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
setSelectedPermission(undefined)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return isLoading ? (
|
return isLoading ? (
|
||||||
<CircularProgress
|
<CircularProgress
|
||||||
@@ -358,22 +36,8 @@ const Permission = () => {
|
|||||||
<Grid container direction="column" spacing={1}>
|
<Grid container direction="column" spacing={1}>
|
||||||
<BootstrapGridItem item xs={12}>
|
<BootstrapGridItem item xs={12}>
|
||||||
<Paper elevation={3} sx={{ display: 'flex' }}>
|
<Paper elevation={3} sx={{ display: 'flex' }}>
|
||||||
<Tooltip title="Filter Permissions">
|
<FilterPermissionsButton />
|
||||||
<IconButton onClick={() => setFilterModalOpen(true)}>
|
{isAdmin && <AddPermissionButton />}
|
||||||
<FilterListIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<Tooltip
|
|
||||||
sx={{ marginLeft: 'auto' }}
|
|
||||||
title="Add Permission"
|
|
||||||
placement="bottom-end"
|
|
||||||
>
|
|
||||||
<IconButton onClick={() => setAddPermissionModalOpen(true)}>
|
|
||||||
<AddIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</BootstrapGridItem>
|
</BootstrapGridItem>
|
||||||
<BootstrapGridItem item xs={12}>
|
<BootstrapGridItem item xs={12}>
|
||||||
@@ -384,192 +48,13 @@ const Permission = () => {
|
|||||||
/>
|
/>
|
||||||
</BootstrapGridItem>
|
</BootstrapGridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
<BootstrapSnackbar
|
<PermissionResponseDialog />
|
||||||
open={openSnackbar}
|
<UpdatePermissionDialog />
|
||||||
setOpen={setOpenSnackbar}
|
<DeletePermissionDialog />
|
||||||
message={snackbarMessage}
|
<Dialog />
|
||||||
severity={snackbarSeverity}
|
<Snackbar />
|
||||||
/>
|
|
||||||
<Modal
|
|
||||||
open={openModal}
|
|
||||||
setOpen={setOpenModal}
|
|
||||||
title={modalTitle}
|
|
||||||
payload={modalPayload}
|
|
||||||
/>
|
|
||||||
<PermissionFilterModal
|
|
||||||
open={filterModalOpen}
|
|
||||||
handleOpen={setFilterModalOpen}
|
|
||||||
permissions={permissions}
|
|
||||||
pathFilter={pathFilter}
|
|
||||||
setPathFilter={setPathFilter}
|
|
||||||
principalFilter={principalFilter}
|
|
||||||
setPrincipalFilter={setPrincipalFilter}
|
|
||||||
principalTypeFilter={principalTypeFilter}
|
|
||||||
setPrincipalTypeFilter={setPrincipalTypeFilter}
|
|
||||||
settingFilter={settingFilter}
|
|
||||||
setSettingFilter={setSettingFilter}
|
|
||||||
applyFilter={applyFilter}
|
|
||||||
resetFilter={resetFilter}
|
|
||||||
/>
|
|
||||||
<AddPermissionModal
|
|
||||||
open={addPermissionModalOpen}
|
|
||||||
handleOpen={setAddPermissionModalOpen}
|
|
||||||
addPermission={addPermission}
|
|
||||||
/>
|
|
||||||
<PermissionResponseModal
|
|
||||||
open={openPermissionResponseModal}
|
|
||||||
setOpen={setOpenPermissionResponseModal}
|
|
||||||
payload={permissionResponsePayload}
|
|
||||||
/>
|
|
||||||
<UpdatePermissionModal
|
|
||||||
open={updatePermissionModalOpen}
|
|
||||||
handleOpen={setUpdatePermissionModalOpen}
|
|
||||||
permission={selectedPermission}
|
|
||||||
updatePermission={updatePermission}
|
|
||||||
/>
|
|
||||||
<DeleteConfirmationModal
|
|
||||||
open={deleteConfirmationModalOpen}
|
|
||||||
setOpen={setDeleteConfirmationModalOpen}
|
|
||||||
message={deleteConfirmationModalMessage}
|
|
||||||
_delete={deletePermission}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Permission
|
export default Permission
|
||||||
|
|
||||||
type PermissionTableProps = {
|
|
||||||
permissions: PermissionResponse[]
|
|
||||||
handleUpdatePermissionClick: (permission: PermissionResponse) => void
|
|
||||||
handleDeletePermissionClick: (permission: PermissionResponse) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PermissionTable = ({
|
|
||||||
permissions,
|
|
||||||
handleUpdatePermissionClick,
|
|
||||||
handleDeletePermissionClick
|
|
||||||
}: PermissionTableProps) => {
|
|
||||||
const appContext = useContext(AppContext)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table sx={{ minWidth: 650 }}>
|
|
||||||
<TableHead sx={{ background: 'rgb(0,0,0, 0.3)' }}>
|
|
||||||
<TableRow>
|
|
||||||
<BootstrapTableCell>Path</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Permission Type</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Principal</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Principal Type</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>Setting</BootstrapTableCell>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<BootstrapTableCell>Action</BootstrapTableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{permissions.map((permission) => (
|
|
||||||
<TableRow key={permission.permissionId}>
|
|
||||||
<BootstrapTableCell>{permission.path}</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>{permission.type}</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>
|
|
||||||
{displayPrincipal(permission)}
|
|
||||||
</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>
|
|
||||||
{displayPrincipalType(permission)}
|
|
||||||
</BootstrapTableCell>
|
|
||||||
<BootstrapTableCell>{permission.setting}</BootstrapTableCell>
|
|
||||||
{appContext.isAdmin && (
|
|
||||||
<BootstrapTableCell>
|
|
||||||
<Tooltip title="Edit Permission">
|
|
||||||
<IconButton
|
|
||||||
onClick={() => handleUpdatePermissionClick(permission)}
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Delete Permission">
|
|
||||||
<IconButton
|
|
||||||
color="error"
|
|
||||||
onClick={() => handleDeletePermissionClick(permission)}
|
|
||||||
>
|
|
||||||
<DeleteForeverIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</BootstrapTableCell>
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayPrincipal = (permission: PermissionResponse) => {
|
|
||||||
if (permission.user) return permission.user.username
|
|
||||||
if (permission.group) return <DisplayGroup group={permission.group} />
|
|
||||||
}
|
|
||||||
|
|
||||||
type DisplayGroupProps = {
|
|
||||||
group: GroupDetailsResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplayGroup = ({ group }: DisplayGroupProps) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
const handlePopoverOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePopoverClose = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = Boolean(anchorEl)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Typography
|
|
||||||
aria-owns={open ? 'mouse-over-popover' : undefined}
|
|
||||||
aria-haspopup="true"
|
|
||||||
onMouseEnter={handlePopoverOpen}
|
|
||||||
onMouseLeave={handlePopoverClose}
|
|
||||||
>
|
|
||||||
{group.name}
|
|
||||||
</Typography>
|
|
||||||
<Popover
|
|
||||||
id="mouse-over-popover"
|
|
||||||
sx={{
|
|
||||||
pointerEvents: 'none'
|
|
||||||
}}
|
|
||||||
open={open}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'left'
|
|
||||||
}}
|
|
||||||
onClose={handlePopoverClose}
|
|
||||||
disableRestoreFocus
|
|
||||||
>
|
|
||||||
<Typography sx={{ p: 1 }} variant="h6" component="div">
|
|
||||||
Group Members
|
|
||||||
</Typography>
|
|
||||||
{group.users.map((user, index) => (
|
|
||||||
<Typography key={index} sx={{ p: 1 }} component="li">
|
|
||||||
{user.username}
|
|
||||||
</Typography>
|
|
||||||
))}
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayPrincipalType = (permission: PermissionResponse) => {
|
|
||||||
if (permission.user) return PrincipalType.User
|
|
||||||
if (permission.group) return PrincipalType.Group
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,54 +1,26 @@
|
|||||||
import React, {
|
import React, { Dispatch, SetStateAction } from 'react'
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useContext
|
|
||||||
} from 'react'
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Backdrop,
|
Backdrop,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Paper,
|
Paper,
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
Tab,
|
Tab,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { styled } from '@mui/material/styles'
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
import {
|
import Editor, { MonacoDiffEditor } from 'react-monaco-editor'
|
||||||
RocketLaunch,
|
|
||||||
MoreVert,
|
|
||||||
Save,
|
|
||||||
SaveAs,
|
|
||||||
Difference,
|
|
||||||
Edit
|
|
||||||
} from '@mui/icons-material'
|
|
||||||
import Editor, {
|
|
||||||
MonacoDiffEditor,
|
|
||||||
DiffEditorDidMount,
|
|
||||||
EditorDidMount,
|
|
||||||
monaco
|
|
||||||
} from 'react-monaco-editor'
|
|
||||||
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
||||||
|
|
||||||
import { AppContext, RunTimeType } from '../../context/appContext'
|
|
||||||
|
|
||||||
import FilePathInputModal from '../../components/filePathInputModal'
|
import FilePathInputModal from '../../components/filePathInputModal'
|
||||||
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
import FileMenu from './internal/components/fileMenu'
|
||||||
import Modal from '../../components/modal'
|
import RunMenu from './internal/components/runMenu'
|
||||||
|
|
||||||
import { usePrompt, useStateWithCallback } from '../../utils/hooks'
|
import { usePrompt } from '../../utils/hooks'
|
||||||
|
import { getLanguageFromExtension } from './internal/helper'
|
||||||
|
import useEditor from './internal/hooks/useEditor'
|
||||||
|
|
||||||
const StyledTabPanel = styled(TabPanel)(() => ({
|
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||||
padding: '10px'
|
padding: '10px'
|
||||||
@@ -69,241 +41,77 @@ type SASjsEditorProps = {
|
|||||||
setTab: Dispatch<SetStateAction<string>>
|
setTab: Dispatch<SetStateAction<string>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = window.location.origin
|
|
||||||
|
|
||||||
const SASjsEditor = ({
|
const SASjsEditor = ({
|
||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
setSelectedFilePath,
|
setSelectedFilePath,
|
||||||
tab,
|
tab,
|
||||||
setTab
|
setTab
|
||||||
}: SASjsEditorProps) => {
|
}: SASjsEditorProps) => {
|
||||||
const appContext = useContext(AppContext)
|
const {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
ctrlPressed,
|
||||||
const [openModal, setOpenModal] = useState(false)
|
fileContent,
|
||||||
const [modalTitle, setModalTitle] = useState('')
|
isLoading,
|
||||||
const [modalPayload, setModalPayload] = useState('')
|
log,
|
||||||
const [openSnackbar, setOpenSnackbar] = useState(false)
|
openFilePathInputModal,
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('')
|
prevFileContent,
|
||||||
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
runTimes,
|
||||||
AlertSeverityType.Success
|
selectedFileExtension,
|
||||||
)
|
selectedRunTime,
|
||||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
showDiff,
|
||||||
const [fileContent, setFileContent] = useState('')
|
webout,
|
||||||
const [log, setLog] = useState('')
|
Dialog,
|
||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
handleChangeRunTime,
|
||||||
const [webout, setWebout] = useState('')
|
handleDiffEditorDidMount,
|
||||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
handleEditorDidMount,
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
handleFilePathInput,
|
||||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
handleKeyDown,
|
||||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
handleKeyUp,
|
||||||
const [showDiff, setShowDiff] = useState(false)
|
handleRunBtnClick,
|
||||||
|
handleTabChange,
|
||||||
const editorRef = useRef(null as any)
|
saveFile,
|
||||||
|
setShowDiff,
|
||||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
setOpenFilePathInputModal,
|
||||||
editorRef.current = editor
|
setFileContent,
|
||||||
editor.focus()
|
Snackbar
|
||||||
editor.addAction({
|
} = useEditor({ selectedFilePath, setSelectedFilePath, setTab })
|
||||||
// An unique identifier of the contributed action.
|
|
||||||
id: 'show-difference',
|
|
||||||
|
|
||||||
// A label of the action that will be presented to the user.
|
|
||||||
label: 'Show Differences',
|
|
||||||
|
|
||||||
// An optional array of keybindings for the action.
|
|
||||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
|
||||||
|
|
||||||
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 (ed) {
|
|
||||||
setShowDiff(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
|
||||||
diffEditor.focus()
|
|
||||||
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
|
|
||||||
setShowDiff(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
usePrompt(
|
usePrompt(
|
||||||
'Changes you made may not be saved.',
|
'Changes you made may not be saved.',
|
||||||
prevFileContent !== fileContent && !!selectedFilePath
|
prevFileContent !== fileContent && !!selectedFilePath
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const fileMenu = (
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
<FileMenu
|
||||||
}, [appContext.runTimes])
|
showDiff={showDiff}
|
||||||
|
setShowDiff={setShowDiff}
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
|
currentFileContent={fileContent}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||||
|
saveFile={saveFile}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const monacoEditor = showDiff ? (
|
||||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
<MonacoDiffEditor
|
||||||
}, [runTimes])
|
height="98%"
|
||||||
|
language={getLanguageFromExtension(selectedFileExtension)}
|
||||||
useEffect(() => {
|
original={prevFileContent}
|
||||||
if (selectedFilePath) {
|
value={fileContent}
|
||||||
setIsLoading(true)
|
editorDidMount={handleDiffEditorDidMount}
|
||||||
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
|
options={{ readOnly: ctrlPressed }}
|
||||||
axios
|
onChange={(val) => setFileContent(val)}
|
||||||
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
|
/>
|
||||||
.then((res: any) => {
|
) : (
|
||||||
setPrevFileContent(res.data)
|
<Editor
|
||||||
setFileContent(res.data)
|
height="98%"
|
||||||
})
|
language={getLanguageFromExtension(selectedFileExtension)}
|
||||||
.catch((err) => {
|
value={fileContent}
|
||||||
setModalTitle('Abort')
|
editorDidMount={handleEditorDidMount}
|
||||||
setModalPayload(
|
options={{ readOnly: ctrlPressed }}
|
||||||
typeof err.response.data === 'object'
|
onChange={(val) => setFileContent(val)}
|
||||||
? JSON.stringify(err.response.data)
|
/>
|
||||||
: err.response.data
|
)
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
.finally(() => setIsLoading(false))
|
|
||||||
} else {
|
|
||||||
const content = localStorage.getItem('fileContent') ?? ''
|
|
||||||
setFileContent(content)
|
|
||||||
}
|
|
||||||
setLog('')
|
|
||||||
setWebout('')
|
|
||||||
setTab('code')
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [selectedFilePath])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (fileContent.length && !selectedFilePath) {
|
|
||||||
localStorage.setItem('fileContent', fileContent)
|
|
||||||
}
|
|
||||||
}, [fileContent, selectedFilePath])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (runTimes.includes(selectedFileExtension))
|
|
||||||
setSelectedRunTime(selectedFileExtension)
|
|
||||||
}, [selectedFileExtension, runTimes])
|
|
||||||
|
|
||||||
const handleTabChange = (_e: any, newValue: string) => {
|
|
||||||
setTab(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSelection = () => {
|
|
||||||
const editor = editorRef.current as any
|
|
||||||
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
|
||||||
return selection ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
|
||||||
|
|
||||||
const runCode = (code: string) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
axios
|
|
||||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
|
||||||
.then((res: any) => {
|
|
||||||
const parsedLog = res?.data?.log
|
|
||||||
.map((logLine: any) => logLine.line)
|
|
||||||
.join('\n')
|
|
||||||
|
|
||||||
setLog(parsedLog)
|
|
||||||
|
|
||||||
setWebout(`${res.data?._webout}`)
|
|
||||||
setTab('log')
|
|
||||||
|
|
||||||
// Scroll to bottom of log
|
|
||||||
const logElement = document.getElementById('log')
|
|
||||||
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
.finally(() => setIsLoading(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
|
||||||
setSelectedRunTime(event.target.value as RunTimeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilePathInput = (filePath: string) => {
|
|
||||||
setOpenFilePathInputModal(false)
|
|
||||||
saveFile(filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveFile = (filePath?: string) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
|
||||||
formData.append('file', stringBlob, 'filename.sas')
|
|
||||||
formData.append('filePath', filePath ?? selectedFilePath)
|
|
||||||
|
|
||||||
const axiosPromise = filePath
|
|
||||||
? axios.post('/SASjsApi/drive/file', formData)
|
|
||||||
: axios.patch('/SASjsApi/drive/file', formData)
|
|
||||||
|
|
||||||
axiosPromise
|
|
||||||
.then(() => {
|
|
||||||
if (filePath && fileContent === prevFileContent) {
|
|
||||||
// when fileContent and prevFileContent is same,
|
|
||||||
// callback function in setPrevFileContent method is not called
|
|
||||||
// because behind the scene useEffect hook is being used
|
|
||||||
// for calling callback function, and it's only fired when the
|
|
||||||
// new value is not equal to old value.
|
|
||||||
// So, we'll have to explicitly update the selected file path
|
|
||||||
|
|
||||||
setSelectedFilePath(filePath, true)
|
|
||||||
} else {
|
|
||||||
setPrevFileContent(fileContent, () => {
|
|
||||||
if (filePath) {
|
|
||||||
setSelectedFilePath(filePath, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setSnackbarMessage('File saved!')
|
|
||||||
setSnackbarSeverity(AlertSeverityType.Success)
|
|
||||||
setOpenSnackbar(true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||||
@@ -316,15 +124,7 @@ const SASjsEditor = ({
|
|||||||
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
|
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
|
||||||
<Box sx={{ marginTop: '10px' }}>
|
<Box sx={{ marginTop: '10px' }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
<FileMenu
|
{fileMenu}
|
||||||
showDiff={showDiff}
|
|
||||||
setShowDiff={setShowDiff}
|
|
||||||
prevFileContent={prevFileContent}
|
|
||||||
currentFileContent={fileContent}
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
|
||||||
saveFile={saveFile}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
@@ -336,26 +136,7 @@ const SASjsEditor = ({
|
|||||||
}}
|
}}
|
||||||
elevation={3}
|
elevation={3}
|
||||||
>
|
>
|
||||||
{showDiff ? (
|
{monacoEditor}
|
||||||
<MonacoDiffEditor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
original={prevFileContent}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleDiffEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Editor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
@@ -392,15 +173,7 @@ const SASjsEditor = ({
|
|||||||
handleChangeRunTime={handleChangeRunTime}
|
handleChangeRunTime={handleChangeRunTime}
|
||||||
handleRunBtnClick={handleRunBtnClick}
|
handleRunBtnClick={handleRunBtnClick}
|
||||||
/>
|
/>
|
||||||
<FileMenu
|
{fileMenu}
|
||||||
showDiff={showDiff}
|
|
||||||
setShowDiff={setShowDiff}
|
|
||||||
prevFileContent={prevFileContent}
|
|
||||||
currentFileContent={fileContent}
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
|
||||||
saveFile={saveFile}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Paper
|
<Paper
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
@@ -413,26 +186,7 @@ const SASjsEditor = ({
|
|||||||
}}
|
}}
|
||||||
elevation={3}
|
elevation={3}
|
||||||
>
|
>
|
||||||
{showDiff ? (
|
{monacoEditor}
|
||||||
<MonacoDiffEditor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
original={prevFileContent}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleDiffEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Editor
|
|
||||||
height="98%"
|
|
||||||
language={getLanguage(selectedFileExtension)}
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => setFileContent(val)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -462,18 +216,8 @@ const SASjsEditor = ({
|
|||||||
</StyledTabPanel>
|
</StyledTabPanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
)}
|
)}
|
||||||
<Modal
|
<Dialog />
|
||||||
open={openModal}
|
<Snackbar />
|
||||||
setOpen={setOpenModal}
|
|
||||||
title={modalTitle}
|
|
||||||
payload={modalPayload}
|
|
||||||
/>
|
|
||||||
<BootstrapSnackbar
|
|
||||||
open={openSnackbar}
|
|
||||||
setOpen={setOpenSnackbar}
|
|
||||||
message={snackbarMessage}
|
|
||||||
severity={snackbarSeverity}
|
|
||||||
/>
|
|
||||||
<FilePathInputModal
|
<FilePathInputModal
|
||||||
open={openFilePathInputModal}
|
open={openFilePathInputModal}
|
||||||
setOpen={setOpenFilePathInputModal}
|
setOpen={setOpenFilePathInputModal}
|
||||||
@@ -484,203 +228,3 @@ const SASjsEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default SASjsEditor
|
export default SASjsEditor
|
||||||
|
|
||||||
type RunMenuProps = {
|
|
||||||
selectedFilePath: string
|
|
||||||
fileContent: string
|
|
||||||
prevFileContent: string
|
|
||||||
selectedRunTime: string
|
|
||||||
runTimes: string[]
|
|
||||||
handleChangeRunTime: (event: SelectChangeEvent) => void
|
|
||||||
handleRunBtnClick: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const RunMenu = ({
|
|
||||||
selectedFilePath,
|
|
||||||
fileContent,
|
|
||||||
prevFileContent,
|
|
||||||
selectedRunTime,
|
|
||||||
runTimes,
|
|
||||||
handleChangeRunTime,
|
|
||||||
handleRunBtnClick
|
|
||||||
}: RunMenuProps) => {
|
|
||||||
const launchProgram = () => {
|
|
||||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip title="CTRL+ENTER will also run code">
|
|
||||||
<Button
|
|
||||||
onClick={handleRunBtnClick}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '5px 5px',
|
|
||||||
minWidth: 'unset'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
draggable="false"
|
|
||||||
style={{ width: '25px' }}
|
|
||||||
src="/running-sas.png"
|
|
||||||
></img>
|
|
||||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
{selectedFilePath ? (
|
|
||||||
<Box sx={{ marginLeft: '10px' }}>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
fileContent !== prevFileContent
|
|
||||||
? 'Save file before launching program'
|
|
||||||
: 'Launch program in new window'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<IconButton
|
|
||||||
disabled={fileContent !== prevFileContent}
|
|
||||||
onClick={launchProgram}
|
|
||||||
>
|
|
||||||
<RocketLaunch />
|
|
||||||
</IconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
|
||||||
<FormControl variant="standard">
|
|
||||||
<Select
|
|
||||||
labelId="run-time-select-label"
|
|
||||||
id="run-time-select"
|
|
||||||
value={selectedRunTime}
|
|
||||||
onChange={handleChangeRunTime}
|
|
||||||
>
|
|
||||||
{runTimes.map((runTime) => (
|
|
||||||
<MenuItem key={runTime} value={runTime}>
|
|
||||||
{runTime}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileMenuProps = {
|
|
||||||
showDiff: boolean
|
|
||||||
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
prevFileContent: string
|
|
||||||
currentFileContent: string
|
|
||||||
selectedFilePath: string
|
|
||||||
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
|
||||||
saveFile: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileMenu = ({
|
|
||||||
showDiff,
|
|
||||||
setShowDiff,
|
|
||||||
prevFileContent,
|
|
||||||
currentFileContent,
|
|
||||||
selectedFilePath,
|
|
||||||
setOpenFilePathInputModal,
|
|
||||||
saveFile
|
|
||||||
}: FileMenuProps) => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState<
|
|
||||||
(EventTarget & HTMLButtonElement) | null
|
|
||||||
>(null)
|
|
||||||
|
|
||||||
const handleMenu = (
|
|
||||||
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
|
||||||
) => {
|
|
||||||
if (event) setAnchorEl(event.currentTarget)
|
|
||||||
else setAnchorEl(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDiffBtnClick = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
setShowDiff(!showDiff)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveAsBtnClick = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
setOpenFilePathInputModal(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveBtnClick = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
saveFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tooltip title="Save File Menu">
|
|
||||||
<IconButton onClick={handleMenu}>
|
|
||||||
<MoreVert />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Menu
|
|
||||||
id="save-file-menu"
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
open={!!anchorEl}
|
|
||||||
onClose={() => handleMenu()}
|
|
||||||
>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
onClick={handleDiffBtnClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={showDiff ? <Edit /> : <Difference />}
|
|
||||||
>
|
|
||||||
{showDiff ? 'Edit' : 'Diff'}
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveBtnClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<Save />}
|
|
||||||
disabled={
|
|
||||||
!selectedFilePath || prevFileContent === currentFileContent
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem sx={{ justifyContent: 'center' }}>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveAsBtnClick}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
startIcon={<SaveAs />}
|
|
||||||
>
|
|
||||||
Save As
|
|
||||||
</Button>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLanguage = (extension: string) => {
|
|
||||||
if (extension === 'js') return 'javascript'
|
|
||||||
|
|
||||||
if (extension === 'ts') return 'typescript'
|
|
||||||
|
|
||||||
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
|
||||||
|
|
||||||
return extension
|
|
||||||
}
|
|
||||||
|
|||||||
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { Button, IconButton, Menu, MenuItem, Tooltip } from '@mui/material'
|
||||||
|
|
||||||
|
import { MoreVert, Save, SaveAs, Difference, Edit } from '@mui/icons-material'
|
||||||
|
|
||||||
|
type FileMenuProps = {
|
||||||
|
showDiff: boolean
|
||||||
|
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
prevFileContent: string
|
||||||
|
currentFileContent: string
|
||||||
|
selectedFilePath: string
|
||||||
|
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
saveFile: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileMenu = ({
|
||||||
|
showDiff,
|
||||||
|
setShowDiff,
|
||||||
|
prevFileContent,
|
||||||
|
currentFileContent,
|
||||||
|
selectedFilePath,
|
||||||
|
setOpenFilePathInputModal,
|
||||||
|
saveFile
|
||||||
|
}: FileMenuProps) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
|
(EventTarget & HTMLButtonElement) | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const handleMenu = (
|
||||||
|
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
if (event) setAnchorEl(event.currentTarget)
|
||||||
|
else setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiffBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setShowDiff(!showDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveAsBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setOpenFilePathInputModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
saveFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Save File Menu">
|
||||||
|
<IconButton onClick={handleMenu}>
|
||||||
|
<MoreVert />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
id="save-file-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
open={!!anchorEl}
|
||||||
|
onClose={() => handleMenu()}
|
||||||
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleDiffBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={showDiff ? <Edit /> : <Difference />}
|
||||||
|
>
|
||||||
|
{showDiff ? 'Edit' : 'Diff'}
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<Save />}
|
||||||
|
disabled={
|
||||||
|
!selectedFilePath || prevFileContent === currentFileContent
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveAsBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SaveAs />}
|
||||||
|
>
|
||||||
|
Save As
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileMenu
|
||||||
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Tooltip
|
||||||
|
} from '@mui/material'
|
||||||
|
|
||||||
|
import { RocketLaunch } from '@mui/icons-material'
|
||||||
|
|
||||||
|
type RunMenuProps = {
|
||||||
|
selectedFilePath: string
|
||||||
|
fileContent: string
|
||||||
|
prevFileContent: string
|
||||||
|
selectedRunTime: string
|
||||||
|
runTimes: string[]
|
||||||
|
handleChangeRunTime: (event: SelectChangeEvent) => void
|
||||||
|
handleRunBtnClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunMenu = ({
|
||||||
|
selectedFilePath,
|
||||||
|
fileContent,
|
||||||
|
prevFileContent,
|
||||||
|
selectedRunTime,
|
||||||
|
runTimes,
|
||||||
|
handleChangeRunTime,
|
||||||
|
handleRunBtnClick
|
||||||
|
}: RunMenuProps) => {
|
||||||
|
const launchProgram = () => {
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="CTRL+ENTER will also run code">
|
||||||
|
<Button
|
||||||
|
onClick={handleRunBtnClick}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '5px 5px',
|
||||||
|
minWidth: 'unset'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
draggable="false"
|
||||||
|
style={{ width: '25px' }}
|
||||||
|
src="/running-sas.png"
|
||||||
|
></img>
|
||||||
|
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{selectedFilePath ? (
|
||||||
|
<Box sx={{ marginLeft: '10px' }}>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
fileContent !== prevFileContent
|
||||||
|
? 'Save file before launching program'
|
||||||
|
: 'Launch program in new window'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={fileContent !== prevFileContent}
|
||||||
|
onClick={launchProgram}
|
||||||
|
>
|
||||||
|
<RocketLaunch />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
||||||
|
<FormControl variant="standard">
|
||||||
|
<Select
|
||||||
|
labelId="run-time-select-label"
|
||||||
|
id="run-time-select"
|
||||||
|
value={selectedRunTime}
|
||||||
|
onChange={handleChangeRunTime}
|
||||||
|
>
|
||||||
|
{runTimes.map((runTime) => (
|
||||||
|
<MenuItem key={runTime} value={runTime}>
|
||||||
|
{runTime}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RunMenu
|
||||||
14
web/src/containers/Studio/internal/helper.ts
Normal file
14
web/src/containers/Studio/internal/helper.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export const getLanguageFromExtension = (extension: string) => {
|
||||||
|
if (extension === 'js') return 'javascript'
|
||||||
|
|
||||||
|
if (extension === 'ts') return 'typescript'
|
||||||
|
|
||||||
|
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
||||||
|
|
||||||
|
return extension
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSelection = (editor: any) => {
|
||||||
|
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||||
|
return selection ?? ''
|
||||||
|
}
|
||||||
299
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
299
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
|
import { DiffEditorDidMount, EditorDidMount, monaco } from 'react-monaco-editor'
|
||||||
|
import { SelectChangeEvent } from '@mui/material'
|
||||||
|
import { getSelection } from '../helper'
|
||||||
|
import { AppContext, RunTimeType } from '../../../../context/appContext'
|
||||||
|
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||||
|
import {
|
||||||
|
useModal,
|
||||||
|
useSnackbar,
|
||||||
|
useStateWithCallback
|
||||||
|
} from '../../../../utils/hooks'
|
||||||
|
|
||||||
|
const SASJS_LOGS_SEPARATOR =
|
||||||
|
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
|
type UseEditorParams = {
|
||||||
|
selectedFilePath: string
|
||||||
|
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||||
|
setTab: Dispatch<SetStateAction<string>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const useEditor = ({
|
||||||
|
selectedFilePath,
|
||||||
|
setSelectedFilePath,
|
||||||
|
setTab
|
||||||
|
}: UseEditorParams) => {
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
|
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
|
||||||
|
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||||
|
useSnackbar()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||||
|
const [fileContent, setFileContent] = useState('')
|
||||||
|
const [log, setLog] = useState('')
|
||||||
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
|
const [webout, setWebout] = useState('')
|
||||||
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
|
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||||
|
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||||
|
const [showDiff, setShowDiff] = useState(false)
|
||||||
|
|
||||||
|
const editorRef = useRef(null as any)
|
||||||
|
|
||||||
|
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||||
|
editorRef.current = editor
|
||||||
|
editor.focus()
|
||||||
|
editor.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'show-difference',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Show Differences',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
||||||
|
|
||||||
|
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 (ed) {
|
||||||
|
setShowDiff(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
||||||
|
diffEditor.focus()
|
||||||
|
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
|
||||||
|
setShowDiff(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFile = useCallback(
|
||||||
|
(filePath?: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob)
|
||||||
|
formData.append('filePath', filePath ?? selectedFilePath)
|
||||||
|
|
||||||
|
const axiosPromise = filePath
|
||||||
|
? axios.post('/SASjsApi/drive/file', formData)
|
||||||
|
: axios.patch('/SASjsApi/drive/file', formData)
|
||||||
|
|
||||||
|
axiosPromise
|
||||||
|
.then(() => {
|
||||||
|
if (filePath && fileContent === prevFileContent) {
|
||||||
|
// when fileContent and prevFileContent is same,
|
||||||
|
// callback function in setPrevFileContent method is not called
|
||||||
|
// because behind the scene useEffect hook is being used
|
||||||
|
// for calling callback function, and it's only fired when the
|
||||||
|
// new value is not equal to old value.
|
||||||
|
// So, we'll have to explicitly update the selected file path
|
||||||
|
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
} else {
|
||||||
|
setPrevFileContent(fileContent, () => {
|
||||||
|
if (filePath) {
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setSnackbarMessage('File saved!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[fileContent, prevFileContent, selectedFilePath]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTabChange = (_e: any, newValue: string) => {
|
||||||
|
setTab(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRunBtnClick = () =>
|
||||||
|
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||||
|
|
||||||
|
const runCode = (code: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||||
|
.then((res: any) => {
|
||||||
|
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||||
|
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||||
|
setTab('log')
|
||||||
|
|
||||||
|
// Scroll to bottom of log
|
||||||
|
const logElement = document.getElementById('log')
|
||||||
|
if (logElement) logElement.scrollTop = logElement.scrollHeight
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: any) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
if (event.key === 'v') {
|
||||||
|
setCtrlPressed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
setSelectedRunTime(event.target.value as RunTimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilePathInput = (filePath: string) => {
|
||||||
|
setOpenFilePathInputModal(false)
|
||||||
|
saveFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorRef.current.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'save-file',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Save',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||||
|
|
||||||
|
// Method that will be executed when the action is triggered.
|
||||||
|
// @param editor The editor instance is passed in as a convenience
|
||||||
|
run: () => {
|
||||||
|
if (!selectedFilePath) return setOpenFilePathInputModal(true)
|
||||||
|
if (prevFileContent !== fileContent) return saveFile()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
|
}, [appContext.runTimes])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
||||||
|
}, [runTimes])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFilePath) {
|
||||||
|
setIsLoading(true)
|
||||||
|
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
|
||||||
|
axios
|
||||||
|
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
|
||||||
|
.then((res: any) => {
|
||||||
|
setPrevFileContent(res.data)
|
||||||
|
setFileContent(res.data)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
} else {
|
||||||
|
const content = localStorage.getItem('fileContent') ?? ''
|
||||||
|
setFileContent(content)
|
||||||
|
}
|
||||||
|
setLog('')
|
||||||
|
setWebout('')
|
||||||
|
setTab('code')
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedFilePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (fileContent.length && !selectedFilePath) {
|
||||||
|
localStorage.setItem('fileContent', fileContent)
|
||||||
|
}
|
||||||
|
}, [fileContent, selectedFilePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (runTimes.includes(selectedFileExtension))
|
||||||
|
setSelectedRunTime(selectedFileExtension)
|
||||||
|
}, [selectedFileExtension, runTimes])
|
||||||
|
|
||||||
|
return {
|
||||||
|
ctrlPressed,
|
||||||
|
fileContent,
|
||||||
|
isLoading,
|
||||||
|
log,
|
||||||
|
openFilePathInputModal,
|
||||||
|
prevFileContent,
|
||||||
|
runTimes,
|
||||||
|
selectedFileExtension,
|
||||||
|
selectedRunTime,
|
||||||
|
showDiff,
|
||||||
|
webout,
|
||||||
|
Dialog,
|
||||||
|
handleChangeRunTime,
|
||||||
|
handleDiffEditorDidMount,
|
||||||
|
handleEditorDidMount,
|
||||||
|
handleFilePathInput,
|
||||||
|
handleKeyDown,
|
||||||
|
handleKeyUp,
|
||||||
|
handleRunBtnClick,
|
||||||
|
handleTabChange,
|
||||||
|
saveFile,
|
||||||
|
setShowDiff,
|
||||||
|
setOpenFilePathInputModal,
|
||||||
|
setFileContent,
|
||||||
|
Snackbar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useEditor
|
||||||
@@ -16,7 +16,9 @@ export enum ModeType {
|
|||||||
|
|
||||||
export enum RunTimeType {
|
export enum RunTimeType {
|
||||||
SAS = 'sas',
|
SAS = 'sas',
|
||||||
JS = 'js'
|
JS = 'js',
|
||||||
|
PY = 'py',
|
||||||
|
R = 'r'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppContextProps {
|
interface AppContextProps {
|
||||||
|
|||||||
120
web/src/context/permissionsContext.tsx
Normal file
120
web/src/context/permissionsContext.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
ReactNode
|
||||||
|
} from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { PermissionResponse } from '../utils/types'
|
||||||
|
import { useModal, useSnackbar } from '../utils/hooks'
|
||||||
|
import { AlertSeverityType } from '../components/snackbar'
|
||||||
|
import usePermissionResponseModal from '../containers/Settings/internal/hooks/usePermissionResponseModal'
|
||||||
|
import { PermissionResponsePayload } from '../containers/Settings/internal/components/permissionResponseModal'
|
||||||
|
|
||||||
|
interface PermissionsContextProps {
|
||||||
|
isLoading: boolean
|
||||||
|
setIsLoading: Dispatch<SetStateAction<boolean>>
|
||||||
|
permissions: PermissionResponse[]
|
||||||
|
setPermissions: Dispatch<React.SetStateAction<PermissionResponse[]>>
|
||||||
|
selectedPermission: PermissionResponse | undefined
|
||||||
|
setSelectedPermission: Dispatch<
|
||||||
|
React.SetStateAction<PermissionResponse | undefined>
|
||||||
|
>
|
||||||
|
filteredPermissions: PermissionResponse[]
|
||||||
|
setFilteredPermissions: Dispatch<React.SetStateAction<PermissionResponse[]>>
|
||||||
|
filterApplied: boolean
|
||||||
|
setFilterApplied: Dispatch<SetStateAction<boolean>>
|
||||||
|
fetchPermissions: () => void
|
||||||
|
Dialog: () => JSX.Element
|
||||||
|
setOpenModal: Dispatch<SetStateAction<boolean>>
|
||||||
|
setModalTitle: Dispatch<SetStateAction<string>>
|
||||||
|
setModalPayload: Dispatch<SetStateAction<string>>
|
||||||
|
Snackbar: () => JSX.Element
|
||||||
|
setOpenSnackbar: Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setSnackbarMessage: Dispatch<React.SetStateAction<string>>
|
||||||
|
setSnackbarSeverity: Dispatch<React.SetStateAction<AlertSeverityType>>
|
||||||
|
PermissionResponseDialog: () => JSX.Element
|
||||||
|
setOpenPermissionResponseModal: Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setPermissionResponsePayload: Dispatch<
|
||||||
|
React.SetStateAction<PermissionResponsePayload>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionsContext = createContext<PermissionsContextProps>(
|
||||||
|
undefined!
|
||||||
|
)
|
||||||
|
|
||||||
|
const PermissionsContextProvider = (props: { children: ReactNode }) => {
|
||||||
|
const { children } = props
|
||||||
|
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
|
||||||
|
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||||
|
useSnackbar()
|
||||||
|
const {
|
||||||
|
PermissionResponseDialog,
|
||||||
|
setOpenPermissionResponseModal,
|
||||||
|
setPermissionResponsePayload
|
||||||
|
} = usePermissionResponseModal()
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [permissions, setPermissions] = useState<PermissionResponse[]>([])
|
||||||
|
const [selectedPermission, setSelectedPermission] =
|
||||||
|
useState<PermissionResponse>()
|
||||||
|
const [filteredPermissions, setFilteredPermissions] = useState<
|
||||||
|
PermissionResponse[]
|
||||||
|
>([])
|
||||||
|
const [filterApplied, setFilterApplied] = useState(false)
|
||||||
|
|
||||||
|
const fetchPermissions = useCallback(() => {
|
||||||
|
axios
|
||||||
|
.get(`/SASjsApi/permission`)
|
||||||
|
.then((res: any) => {
|
||||||
|
if (res.data?.length > 0) {
|
||||||
|
setPermissions(res.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PermissionsContext.Provider
|
||||||
|
value={{
|
||||||
|
isLoading,
|
||||||
|
permissions,
|
||||||
|
selectedPermission,
|
||||||
|
filteredPermissions,
|
||||||
|
filterApplied,
|
||||||
|
Dialog,
|
||||||
|
Snackbar,
|
||||||
|
PermissionResponseDialog,
|
||||||
|
fetchPermissions,
|
||||||
|
setIsLoading,
|
||||||
|
setPermissions,
|
||||||
|
setSelectedPermission,
|
||||||
|
setFilteredPermissions,
|
||||||
|
setFilterApplied,
|
||||||
|
setOpenModal,
|
||||||
|
setModalTitle,
|
||||||
|
setModalPayload,
|
||||||
|
setOpenPermissionResponseModal,
|
||||||
|
setPermissionResponsePayload,
|
||||||
|
setOpenSnackbar,
|
||||||
|
setSnackbarMessage,
|
||||||
|
setSnackbarSeverity
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PermissionsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionsContextProvider
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
|
export * from './useModal'
|
||||||
export * from './usePrompt'
|
export * from './usePrompt'
|
||||||
export * from './useStateWithCallback'
|
export * from './useStateWithCallback'
|
||||||
|
export * from './useSnackbar'
|
||||||
|
|||||||
19
web/src/utils/hooks/useModal.tsx
Normal file
19
web/src/utils/hooks/useModal.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import Modal from '../../components/modal'
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const [openModal, setOpenModal] = useState(false)
|
||||||
|
const [modalTitle, setModalTitle] = useState('')
|
||||||
|
const [modalPayload, setModalPayload] = useState('')
|
||||||
|
|
||||||
|
const Dialog = () => (
|
||||||
|
<Modal
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
title={modalTitle}
|
||||||
|
payload={modalPayload}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { Dialog, setOpenModal, setModalTitle, setModalPayload }
|
||||||
|
}
|
||||||
21
web/src/utils/hooks/useSnackbar.tsx
Normal file
21
web/src/utils/hooks/useSnackbar.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
|
|
||||||
|
export const useSnackbar = () => {
|
||||||
|
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||||
|
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||||
|
AlertSeverityType.Success
|
||||||
|
)
|
||||||
|
|
||||||
|
const Snackbar = () => (
|
||||||
|
<BootstrapSnackbar
|
||||||
|
open={openSnackbar}
|
||||||
|
setOpen={setOpenSnackbar}
|
||||||
|
message={snackbarMessage}
|
||||||
|
severity={snackbarSeverity}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user