From b9d032f148c0dde0a1d0c5b08e1f881bbe02a8b4 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 6 Sep 2022 21:51:17 +0500 Subject: [PATCH 1/5] chore: update swagger.yaml --- api/public/swagger.yaml | 62 ++++++++--------------------------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 7467ba6..5bd8ca1 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -62,53 +62,12 @@ components: - clientSecret type: object additionalProperties: false - IRecordOfAny: - properties: {} - type: object - additionalProperties: {} - LogLine: - properties: - line: - type: string - required: - - line - type: object - additionalProperties: false - HTTPHeaders: - properties: {} - type: object - additionalProperties: - type: string - ExecuteReturnJsonResponse: - properties: - status: - type: string - _webout: - anyOf: - - - type: string - - - $ref: '#/components/schemas/IRecordOfAny' - log: - items: - $ref: '#/components/schemas/LogLine' - type: array - message: - type: string - httpHeaders: - $ref: '#/components/schemas/HTTPHeaders' - required: - - status - - _webout - - log - - httpHeaders - type: object - additionalProperties: false RunTimeType: enum: - sas - js - py + - r type: string ExecuteCodePayload: properties: @@ -550,7 +509,7 @@ components: - setting type: object additionalProperties: false - ExecuteReturnJsonPayload: + ExecutePostRequestPayload: properties: _program: type: string @@ -698,7 +657,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonResponse' + anyOf: + - {type: string} + - {type: string, format: byte} description: 'Execute SAS code.' summary: 'Run SAS Code and returns log' tags: @@ -1687,7 +1648,7 @@ paths: parameters: [] /SASjsApi/stp/execute: get: - operationId: ExecuteReturnRaw + operationId: ExecuteGetRequest responses: '200': description: Ok @@ -1714,17 +1675,16 @@ paths: type: string example: /Projects/myApp/some/program post: - operationId: ExecuteReturnJson + operationId: ExecutePostRequest responses: '200': description: Ok content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonResponse' - examples: - 'Example 1': - value: {status: success, _webout: 'webout content', log: [], httpHeaders: {Content-type: application/zip, Cache-Control: 'public, max-age=1000'}} + anyOf: + - {type: string} + - {type: string, format: byte} description: "Trigger a SAS or JS program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms\n\nThe response will be a JSON object with the following root attributes:\nlog, webout, headers.\n\nThe webout attribute will be nested JSON ONLY if the response-header\ncontains a content-type of application/json AND it is valid JSON.\nOtherwise it will be a stringified version of the webout content." summary: 'Execute a Stored Program, return a JSON object' tags: @@ -1746,7 +1706,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/ExecuteReturnJsonPayload' + $ref: '#/components/schemas/ExecutePostRequestPayload' /: get: operationId: Home From d6651bbdbeee5067f53c36e69a0eefa973c523b6 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Tue, 6 Sep 2022 21:52:21 +0500 Subject: [PATCH 2/5] feat: add support for R stored programs --- README.md | 9 +- api/.env.example | 1 + api/src/controllers/internal/Session.ts | 50 +++++- .../controllers/internal/createRProgram.ts | 68 ++++++++ api/src/controllers/internal/index.ts | 1 + .../controllers/internal/processProgram.ts | 155 +++++++++--------- api/src/types/system/process.d.ts | 2 + api/src/utils/getDesktopFields.ts | 32 +++- api/src/utils/getRunTimeAndFilePath.ts | 2 +- api/src/utils/setProcessVariables.ts | 4 +- api/src/utils/upload.ts | 27 +++ api/src/utils/verifyEnvVariables.ts | 10 +- web/src/context/appContext.tsx | 4 +- 13 files changed, 277 insertions(+), 88 deletions(-) create mode 100644 api/src/controllers/internal/createRProgram.ts diff --git a/README.md b/README.md index 8364747..3d7aa61 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,11 @@ MODE= # Possible options at the moment are sas and js # 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, -# 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 -# 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= # 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 PYTHON_PATH=/usr/bin/python +# Path to Rscript +RSCRIPT_PATH=/usr/bin/Rscript + # Path to working directory # This location is for SAS WORK, staged files, DRIVE, configuration etc SASJS_ROOT=./sasjs_root diff --git a/api/.env.example b/api/.env.example index a8d7980..7c375db 100644 --- a/api/.env.example +++ b/api/.env.example @@ -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 NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node PYTHON_PATH=/usr/bin/python +RSCRIPT_PATH=/usr/bin/Rscript SASJS_ROOT=./sasjs_root diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index dd9f904..a62a019 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -231,9 +231,45 @@ export class PythonSessionController extends SessionController { } } +export class RSessionController extends SessionController { + protected async createSession(): Promise { + 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 = ( runTime: RunTimeType -): SASSessionController | JSSessionController | PythonSessionController => { +): + | SASSessionController + | JSSessionController + | PythonSessionController + | RSessionController => { if (runTime === RunTimeType.SAS) { return getSASSessionController() } @@ -246,6 +282,10 @@ export const getSessionController = ( return getPythonSessionController() } + if (runTime === RunTimeType.R) { + return getRSessionController() + } + throw new Error('No Runtime is configured') } @@ -273,6 +313,14 @@ const getPythonSessionController = (): PythonSessionController => { return process.pythonSessionController } +const getRSessionController = (): RSessionController => { + if (process.rSessionController) return process.rSessionController + + process.rSessionController = new RSessionController() + + return process.rSessionController +} + const autoExecContent = ` data _null_; /* remove the dummy SYSIN */ diff --git a/api/src/controllers/internal/createRProgram.ts b/api/src/controllers/internal/createRProgram.ts new file mode 100644 index 0000000..33af81c --- /dev/null +++ b/api/src/controllers/internal/createRProgram.ts @@ -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 +} diff --git a/api/src/controllers/internal/index.ts b/api/src/controllers/internal/index.ts index 2a64677..dd2f702 100644 --- a/api/src/controllers/internal/index.ts +++ b/api/src/controllers/internal/index.ts @@ -5,4 +5,5 @@ export * from './FileUploadController' export * from './createSASProgram' export * from './createJSProgram' export * from './createPythonProgram' +export * from './createRProgram' export * from './processProgram' diff --git a/api/src/controllers/internal/processProgram.ts b/api/src/controllers/internal/processProgram.ts index db2db10..6bd017c 100644 --- a/api/src/controllers/internal/processProgram.ts +++ b/api/src/controllers/internal/processProgram.ts @@ -9,7 +9,8 @@ import { ExecutionVars, createSASProgram, createJSProgram, - createPythonProgram + createPythonProgram, + createRProgram } from './' export const processProgram = async ( @@ -24,81 +25,7 @@ export const processProgram = async ( logPath: string, otherArgs?: any ) => { - if (runTime === RunTimeType.JS) { - 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 { + if (runTime === RunTimeType.SAS) { program = await createSASProgram( program, preProgramVariables, @@ -124,6 +51,82 @@ export const processProgram = async ( while (!session.completed) { 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.rscriptLoc! + + 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) + } } } diff --git a/api/src/types/system/process.d.ts b/api/src/types/system/process.d.ts index 75a16c9..4be0801 100644 --- a/api/src/types/system/process.d.ts +++ b/api/src/types/system/process.d.ts @@ -3,12 +3,14 @@ declare namespace NodeJS { sasLoc?: string nodeLoc?: string pythonLoc?: string + rscriptLoc?: string driveLoc: string logsLoc: string logsUUID: string sasSessionController?: import('../../controllers/internal').SASSessionController jsSessionController?: import('../../controllers/internal').JSSessionController pythonSessionController?: import('../../controllers/internal').PythonSessionController + rSessionController?: import('../../controllers/internal').RSessionController appStreamConfig: import('../').AppStreamConfig logger: import('@sasjs/utils/logger').Logger runTimes: import('../../utils').RunTimeType[] diff --git a/api/src/utils/getDesktopFields.ts b/api/src/utils/getDesktopFields.ts index 5e13264..fe2604a 100644 --- a/api/src/utils/getDesktopFields.ts +++ b/api/src/utils/getDesktopFields.ts @@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils' import { RunTimeType } from './verifyEnvVariables' export const getDesktopFields = async () => { - const { SAS_PATH, NODE_PATH, PYTHON_PATH } = process.env + const { SAS_PATH, NODE_PATH, PYTHON_PATH, RSCRIPT_PATH } = process.env - let sasLoc, nodeLoc, pythonLoc + let sasLoc, nodeLoc, pythonLoc, rscriptLoc if (process.runTimes.includes(RunTimeType.SAS)) { sasLoc = SAS_PATH ?? (await getSASLocation()) @@ -20,7 +20,11 @@ export const getDesktopFields = async () => { pythonLoc = PYTHON_PATH ?? (await getPythonLocation()) } - return { sasLoc, nodeLoc, pythonLoc } + if (process.runTimes.includes(RunTimeType.R)) { + rscriptLoc = RSCRIPT_PATH ?? (await getRScriptLocation()) + } + + return { sasLoc, nodeLoc, pythonLoc, rscriptLoc } } const getDriveLocation = async (): Promise => { @@ -117,3 +121,25 @@ const getPythonLocation = async (): Promise => { return targetName } + +const getRScriptLocation = async (): Promise => { + const validator = async (filePath: string) => { + if (!filePath) return 'Path to RScript 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 Rscript executable: ', + validator, + defaultLocation + ) + + return targetName +} diff --git a/api/src/utils/getRunTimeAndFilePath.ts b/api/src/utils/getRunTimeAndFilePath.ts index e27fb2e..340892a 100644 --- a/api/src/utils/getRunTimeAndFilePath.ts +++ b/api/src/utils/getRunTimeAndFilePath.ts @@ -5,7 +5,7 @@ import { RunTimeType } from '.' export const getRunTimeAndFilePath = async (programPath: string) => { 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 if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) { const runTime = ext.slice(1) diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index 00382b9..8192888 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -29,12 +29,14 @@ export const setProcessVariables = async () => { process.sasLoc = process.env.SAS_PATH process.nodeLoc = process.env.NODE_PATH process.pythonLoc = process.env.PYTHON_PATH + process.rscriptLoc = process.env.RSCRIPT_PATH } else { - const { sasLoc, nodeLoc, pythonLoc } = await getDesktopFields() + const { sasLoc, nodeLoc, pythonLoc, rscriptLoc } = await getDesktopFields() process.sasLoc = sasLoc process.nodeLoc = nodeLoc process.pythonLoc = pythonLoc + process.rscriptLoc = rscriptLoc } const { SASJS_ROOT } = process.env diff --git a/api/src/utils/upload.ts b/api/src/utils/upload.ts index 648f961..d4a4119 100644 --- a/api/src/utils/upload.ts +++ b/api/src/utils/upload.ts @@ -157,3 +157,30 @@ export const generateFileUploadPythonCode = async ( 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 +} diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index b446b4b..65f3cdb 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -34,7 +34,8 @@ export enum LOG_FORMAT_MORGANType { export enum RunTimeType { SAS = 'sas', JS = 'js', - PY = 'py' + PY = 'py', + R = 'r' } export enum ReturnCode { @@ -253,7 +254,8 @@ const verifyRUN_TIMES = (): string[] => { const verifyExecutablePaths = () => { const errors: string[] = [] - const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, MODE } = process.env + const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, RSCRIPT_PATH, MODE } = + process.env if (MODE === ModeType.Server) { const runTimes = RUN_TIMES?.split(',') @@ -269,6 +271,10 @@ const verifyExecutablePaths = () => { if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) { errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`) } + + if (runTimes?.includes(RunTimeType.R) && !RSCRIPT_PATH) { + errors.push(`- RSCRIPT_PATH is required for ${RunTimeType.R} run time`) + } } return errors diff --git a/web/src/context/appContext.tsx b/web/src/context/appContext.tsx index dee793e..b43aad2 100644 --- a/web/src/context/appContext.tsx +++ b/web/src/context/appContext.tsx @@ -16,7 +16,9 @@ export enum ModeType { export enum RunTimeType { SAS = 'sas', - JS = 'js' + JS = 'js', + PY = 'py', + R = 'r' } interface AppContextProps { From 4560ef942fdfb6792f21e36bd44ed2637ebf92e7 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Thu, 8 Sep 2022 21:49:35 +0500 Subject: [PATCH 3/5] chore(web): refactor react code --- web/src/containers/Settings/index.tsx | 5 +- .../internal/components/addPermission.tsx | 40 ++ .../components}/addPermissionModal.tsx | 16 +- .../internal/components/displayGroup.tsx | 63 ++ .../internal/components/filterPermissions.tsx | 72 ++ .../components}/permissionFilterModal.tsx | 6 +- .../components/permissionResponseModal.tsx} | 6 +- .../internal/components/permissionTable.tsx | 101 +++ .../components}/updatePermissionModal.tsx | 17 +- .../containers/Settings/internal/helper.tsx | 13 + .../internal/hooks/useAddPermission.tsx | 109 +++ .../hooks/useDeletePermissionModal.tsx | 61 ++ .../internal/hooks/useFilterPermissions.tsx | 105 +++ .../Settings/internal/hooks/usePermission.ts | 71 ++ .../hooks/usePermissionResponseModal.tsx | 36 + .../hooks/useUpdatePermissionModal.tsx | 63 ++ web/src/containers/Settings/permission.tsx | 567 +--------------- web/src/containers/Studio/editor.tsx | 623 ++---------------- .../Studio/internal/components/fileMenu.tsx | 112 ++++ .../Studio/internal/components/runMenu.tsx | 100 +++ web/src/containers/Studio/internal/helper.ts | 14 + .../Studio/internal/hooks/useEditor.ts | 299 +++++++++ web/src/context/permissionsContext.tsx | 120 ++++ web/src/utils/hooks/index.ts | 2 + web/src/utils/hooks/useModal.tsx | 19 + web/src/utils/hooks/useSnackbar.tsx | 21 + 26 files changed, 1534 insertions(+), 1127 deletions(-) create mode 100644 web/src/containers/Settings/internal/components/addPermission.tsx rename web/src/containers/Settings/{ => internal/components}/addPermissionModal.tsx (95%) create mode 100644 web/src/containers/Settings/internal/components/displayGroup.tsx create mode 100644 web/src/containers/Settings/internal/components/filterPermissions.tsx rename web/src/containers/Settings/{ => internal/components}/permissionFilterModal.tsx (95%) rename web/src/containers/Settings/{addPermissionResponseModal.tsx => internal/components/permissionResponseModal.tsx} (94%) create mode 100644 web/src/containers/Settings/internal/components/permissionTable.tsx rename web/src/containers/Settings/{ => internal/components}/updatePermissionModal.tsx (83%) create mode 100644 web/src/containers/Settings/internal/helper.tsx create mode 100644 web/src/containers/Settings/internal/hooks/useAddPermission.tsx create mode 100644 web/src/containers/Settings/internal/hooks/useDeletePermissionModal.tsx create mode 100644 web/src/containers/Settings/internal/hooks/useFilterPermissions.tsx create mode 100644 web/src/containers/Settings/internal/hooks/usePermission.ts create mode 100644 web/src/containers/Settings/internal/hooks/usePermissionResponseModal.tsx create mode 100644 web/src/containers/Settings/internal/hooks/useUpdatePermissionModal.tsx create mode 100644 web/src/containers/Studio/internal/components/fileMenu.tsx create mode 100644 web/src/containers/Studio/internal/components/runMenu.tsx create mode 100644 web/src/containers/Studio/internal/helper.ts create mode 100644 web/src/containers/Studio/internal/hooks/useEditor.ts create mode 100644 web/src/context/permissionsContext.tsx create mode 100644 web/src/utils/hooks/useModal.tsx create mode 100644 web/src/utils/hooks/useSnackbar.tsx diff --git a/web/src/containers/Settings/index.tsx b/web/src/containers/Settings/index.tsx index 0032549..9cbb183 100644 --- a/web/src/containers/Settings/index.tsx +++ b/web/src/containers/Settings/index.tsx @@ -9,6 +9,7 @@ import Permission from './permission' import Profile from './profile' import { AppContext, ModeType } from '../../context/appContext' +import PermissionsContextProvider from '../../context/permissionsContext' const StyledTab = styled(Tab)({ background: 'black', @@ -64,7 +65,9 @@ const Settings = () => { - + + + diff --git a/web/src/containers/Settings/internal/components/addPermission.tsx b/web/src/containers/Settings/internal/components/addPermission.tsx new file mode 100644 index 0000000..c9fe546 --- /dev/null +++ b/web/src/containers/Settings/internal/components/addPermission.tsx @@ -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> + addPermission: ( + permissionsToAdd: RegisterPermissionPayload[], + permissionType: string, + principalType: string, + principal: string, + permissionSetting: string + ) => Promise +} + +const AddPermission = ({ openModal, setOpenModal, addPermission }: Props) => { + return ( + <> + + setOpenModal(true)}> + + + + + + ) +} + +export default AddPermission diff --git a/web/src/containers/Settings/addPermissionModal.tsx b/web/src/containers/Settings/internal/components/addPermissionModal.tsx similarity index 95% rename from web/src/containers/Settings/addPermissionModal.tsx rename to web/src/containers/Settings/internal/components/addPermissionModal.tsx index 945ed46..2996f4e 100644 --- a/web/src/containers/Settings/addPermissionModal.tsx +++ b/web/src/containers/Settings/internal/components/addPermissionModal.tsx @@ -3,31 +3,21 @@ import axios from 'axios' import { Button, Grid, - Dialog, DialogContent, DialogActions, TextField, CircularProgress, Autocomplete } 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 { UserResponse, GroupResponse, RegisterPermissionPayload -} from '../../utils/types' - -const BootstrapDialog = styled(Dialog)(({ theme }) => ({ - '& .MuiDialogContent-root': { - padding: theme.spacing(2) - }, - '& .MuiDialogActions-root': { - padding: theme.spacing(1) - } -})) +} from '../../../../utils/types' type AddPermissionModalProps = { open: boolean diff --git a/web/src/containers/Settings/internal/components/displayGroup.tsx b/web/src/containers/Settings/internal/components/displayGroup.tsx new file mode 100644 index 0000000..058ab51 --- /dev/null +++ b/web/src/containers/Settings/internal/components/displayGroup.tsx @@ -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(null) + + const handlePopoverOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handlePopoverClose = () => { + setAnchorEl(null) + } + + const open = Boolean(anchorEl) + + return ( +
+ + {group.name} + + + + Group Members + + {group.users.map((user, index) => ( + + {user.username} + + ))} + +
+ ) +} + +export default DisplayGroup diff --git a/web/src/containers/Settings/internal/components/filterPermissions.tsx b/web/src/containers/Settings/internal/components/filterPermissions.tsx new file mode 100644 index 0000000..c599fd5 --- /dev/null +++ b/web/src/containers/Settings/internal/components/filterPermissions.tsx @@ -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> + 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([]) + const [principalFilter, setPrincipalFilter] = useState([]) + const [principalTypeFilter, setPrincipalTypeFilter] = useState< + PrincipalType[] + >([]) + const [settingFilter, setSettingFilter] = useState([]) + const handleApplyFilter = () => { + applyFilter(pathFilter, principalFilter, principalTypeFilter, settingFilter) + } + + const handleResetFilter = () => { + setPathFilter([]) + setPrincipalFilter([]) + setPrincipalFilter([]) + setSettingFilter([]) + resetFilter() + } + + return ( + <> + + handleOpen(true)}> + + + + + + ) +} + +export default FilterPermissions diff --git a/web/src/containers/Settings/permissionFilterModal.tsx b/web/src/containers/Settings/internal/components/permissionFilterModal.tsx similarity index 95% rename from web/src/containers/Settings/permissionFilterModal.tsx rename to web/src/containers/Settings/internal/components/permissionFilterModal.tsx index 7fe64fb..22915e4 100644 --- a/web/src/containers/Settings/permissionFilterModal.tsx +++ b/web/src/containers/Settings/internal/components/permissionFilterModal.tsx @@ -10,9 +10,9 @@ import { import { styled } from '@mui/material/styles' import Autocomplete from '@mui/material/Autocomplete' -import { PermissionResponse } from '../../utils/types' -import { BootstrapDialogTitle } from '../../components/dialogTitle' -import { PrincipalType } from './permission' +import { PermissionResponse } from '../../../../utils/types' +import { BootstrapDialogTitle } from '../../../../components/dialogTitle' +import { PrincipalType } from '../hooks/usePermission' const BootstrapDialog = styled(Dialog)(({ theme }) => ({ '& .MuiDialogContent-root': { diff --git a/web/src/containers/Settings/addPermissionResponseModal.tsx b/web/src/containers/Settings/internal/components/permissionResponseModal.tsx similarity index 94% rename from web/src/containers/Settings/addPermissionResponseModal.tsx rename to web/src/containers/Settings/internal/components/permissionResponseModal.tsx index 4096ebc..0cc61c9 100644 --- a/web/src/containers/Settings/addPermissionResponseModal.tsx +++ b/web/src/containers/Settings/internal/components/permissionResponseModal.tsx @@ -2,9 +2,9 @@ import React from 'react' import { Typography, DialogContent } from '@mui/material' -import { BootstrapDialog } from '../../components/modal' -import { BootstrapDialogTitle } from '../../components/dialogTitle' -import { PermissionResponse } from '../../utils/types' +import { BootstrapDialog } from '../../../../components/modal' +import { BootstrapDialogTitle } from '../../../../components/dialogTitle' +import { PermissionResponse } from '../../../../utils/types' export interface PermissionResponsePayload { permissionType: string diff --git a/web/src/containers/Settings/internal/components/permissionTable.tsx b/web/src/containers/Settings/internal/components/permissionTable.tsx new file mode 100644 index 0000000..550f72f --- /dev/null +++ b/web/src/containers/Settings/internal/components/permissionTable.tsx @@ -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 ( + + + + + Path + Permission Type + Principal + Principal Type + Setting + {appContext.isAdmin && ( + Action + )} + + + + {permissions.map((permission) => ( + + {permission.path} + {permission.type} + + {displayPrincipal(permission)} + + + {displayPrincipalType(permission)} + + {permission.setting} + {appContext.isAdmin && ( + + + handleUpdatePermissionClick(permission)} + > + + + + + handleDeletePermissionClick(permission)} + > + + + + + )} + + ))} + +
+
+ ) +} + +export default PermissionTable diff --git a/web/src/containers/Settings/updatePermissionModal.tsx b/web/src/containers/Settings/internal/components/updatePermissionModal.tsx similarity index 83% rename from web/src/containers/Settings/updatePermissionModal.tsx rename to web/src/containers/Settings/internal/components/updatePermissionModal.tsx index 55d92de..ed5f6a6 100644 --- a/web/src/containers/Settings/updatePermissionModal.tsx +++ b/web/src/containers/Settings/internal/components/updatePermissionModal.tsx @@ -2,26 +2,17 @@ import React, { useState, Dispatch, SetStateAction, useEffect } from 'react' import { Button, Grid, - Dialog, DialogContent, DialogActions, TextField } from '@mui/material' -import { styled } from '@mui/material/styles' + 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' - -const BootstrapDialog = styled(Dialog)(({ theme }) => ({ - '& .MuiDialogContent-root': { - padding: theme.spacing(2) - }, - '& .MuiDialogActions-root': { - padding: theme.spacing(1) - } -})) +import { PermissionResponse } from '../../../../utils/types' type UpdatePermissionModalProps = { open: boolean diff --git a/web/src/containers/Settings/internal/helper.tsx b/web/src/containers/Settings/internal/helper.tsx new file mode 100644 index 0000000..e6d2519 --- /dev/null +++ b/web/src/containers/Settings/internal/helper.tsx @@ -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 +} + +export const displayPrincipalType = (permission: PermissionResponse) => { + if (permission.user) return PrincipalType.User + if (permission.group) return PrincipalType.Group +} diff --git a/web/src/containers/Settings/internal/hooks/useAddPermission.tsx b/web/src/containers/Settings/internal/hooks/useAddPermission.tsx new file mode 100644 index 0000000..f2d1761 --- /dev/null +++ b/web/src/containers/Settings/internal/hooks/useAddPermission.tsx @@ -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 = () => ( + + ) + + return { AddPermissionButton, setAddPermissionModalOpen } +} + +export default useAddPermission diff --git a/web/src/containers/Settings/internal/hooks/useDeletePermissionModal.tsx b/web/src/containers/Settings/internal/hooks/useDeletePermissionModal.tsx new file mode 100644 index 0000000..7638c22 --- /dev/null +++ b/web/src/containers/Settings/internal/hooks/useDeletePermissionModal.tsx @@ -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 = () => ( + + ) + + return { DeletePermissionDialog, setDeleteConfirmationModalOpen } +} + +export default useDeletePermissionModal diff --git a/web/src/containers/Settings/internal/hooks/useFilterPermissions.tsx b/web/src/containers/Settings/internal/hooks/useFilterPermissions.tsx new file mode 100644 index 0000000..7ca1f6a --- /dev/null +++ b/web/src/containers/Settings/internal/hooks/useFilterPermissions.tsx @@ -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 = () => ( + + ) + + return { FilterPermissionsButton } +} + +export default useFilterPermissions diff --git a/web/src/containers/Settings/internal/hooks/usePermission.ts b/web/src/containers/Settings/internal/hooks/usePermission.ts new file mode 100644 index 0000000..6d85180 --- /dev/null +++ b/web/src/containers/Settings/internal/hooks/usePermission.ts @@ -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 diff --git a/web/src/containers/Settings/internal/hooks/usePermissionResponseModal.tsx b/web/src/containers/Settings/internal/hooks/usePermissionResponseModal.tsx new file mode 100644 index 0000000..ad2d19b --- /dev/null +++ b/web/src/containers/Settings/internal/hooks/usePermissionResponseModal.tsx @@ -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({ + permissionType: '', + principalType: '', + principal: '', + permissionSetting: '', + existingPermissions: [], + newAddedPermissions: [], + updatedPermissions: [], + errorPaths: [] + }) + + const PermissionResponseDialog = () => ( + + ) + + return { + PermissionResponseDialog, + setOpenPermissionResponseModal, + setPermissionResponsePayload + } +} + +export default usePermissionResponseModal diff --git a/web/src/containers/Settings/internal/hooks/useUpdatePermissionModal.tsx b/web/src/containers/Settings/internal/hooks/useUpdatePermissionModal.tsx new file mode 100644 index 0000000..0deceee --- /dev/null +++ b/web/src/containers/Settings/internal/hooks/useUpdatePermissionModal.tsx @@ -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 = () => ( + + ) + + return { UpdatePermissionDialog, setUpdatePermissionModalOpen } +} + +export default useUpdatePermissionModal diff --git a/web/src/containers/Settings/permission.tsx b/web/src/containers/Settings/permission.tsx index dc9f3af..240afcd 100644 --- a/web/src/containers/Settings/permission.tsx +++ b/web/src/containers/Settings/permission.tsx @@ -1,54 +1,7 @@ -import React, { useState, useEffect, useContext, useCallback } from 'react' -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 { Box, Paper, Grid, CircularProgress } from '@mui/material' import { styled } from '@mui/material/styles' - -import Modal from '../../components/modal' -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' -}) +import PermissionTable from './internal/components/permissionTable' +import usePermission from './internal/hooks/usePermission' const BootstrapGridItem = styled(Grid)({ '&.MuiGrid-item': { @@ -56,298 +9,23 @@ const BootstrapGridItem = styled(Grid)({ } }) -export enum PrincipalType { - User = 'User', - Group = 'Group' -} - const Permission = () => { - const appContext = useContext(AppContext) - const [isLoading, setIsLoading] = useState(false) - const [openModal, setOpenModal] = useState(false) - const [modalTitle, setModalTitle] = useState('') - const [modalPayload, setModalPayload] = useState('') - const [openSnackbar, setOpenSnackbar] = useState(false) - const [snackbarMessage, setSnackbarMessage] = useState('') - const [snackbarSeverity, setSnackbarSeverity] = useState( - AlertSeverityType.Success - ) - const [addPermissionModalOpen, setAddPermissionModalOpen] = useState(false) - const [openPermissionResponseModal, setOpenPermissionResponseModal] = - useState(false) - const [permissionResponsePayload, setPermissionResponsePayload] = - useState({ - permissionType: '', - 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() - const [filterModalOpen, setFilterModalOpen] = useState(false) - const [pathFilter, setPathFilter] = useState([]) - const [principalFilter, setPrincipalFilter] = useState([]) - const [principalTypeFilter, setPrincipalTypeFilter] = useState< - PrincipalType[] - >([]) - const [settingFilter, setSettingFilter] = useState([]) - const [permissions, setPermissions] = useState([]) - 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) - }) - } + const { + filterApplied, + filteredPermissions, + isAdmin, + isLoading, + permissions, + AddPermissionButton, + UpdatePermissionDialog, + DeletePermissionDialog, + FilterPermissionsButton, + handleDeletePermissionClick, + handleUpdatePermissionClick, + PermissionResponseDialog, + Dialog, + Snackbar + } = usePermission() return isLoading ? ( { - - setFilterModalOpen(true)}> - - - - {appContext.isAdmin && ( - - setAddPermissionModalOpen(true)}> - - - - )} + + {isAdmin && } @@ -384,192 +48,13 @@ const 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 ( - - - - - Path - Permission Type - Principal - Principal Type - Setting - {appContext.isAdmin && ( - Action - )} - - - - {permissions.map((permission) => ( - - {permission.path} - {permission.type} - - {displayPrincipal(permission)} - - - {displayPrincipalType(permission)} - - {permission.setting} - {appContext.isAdmin && ( - - - handleUpdatePermissionClick(permission)} - > - - - - - handleDeletePermissionClick(permission)} - > - - - - - )} - - ))} - -
-
- ) -} - -const displayPrincipal = (permission: PermissionResponse) => { - if (permission.user) return permission.user.username - if (permission.group) return -} - -type DisplayGroupProps = { - group: GroupDetailsResponse -} - -const DisplayGroup = ({ group }: DisplayGroupProps) => { - const [anchorEl, setAnchorEl] = useState(null) - - const handlePopoverOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget) - } - - const handlePopoverClose = () => { - setAnchorEl(null) - } - - const open = Boolean(anchorEl) - - return ( -
- - {group.name} - - - - Group Members - - {group.users.map((user, index) => ( - - {user.username} - - ))} - -
- ) -} - -const displayPrincipalType = (permission: PermissionResponse) => { - if (permission.user) return PrincipalType.User - if (permission.group) return PrincipalType.Group -} diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx index 5157d35..41ebd47 100644 --- a/web/src/containers/Studio/editor.tsx +++ b/web/src/containers/Studio/editor.tsx @@ -1,55 +1,26 @@ -import React, { - Dispatch, - SetStateAction, - useEffect, - useRef, - useState, - useContext, - useCallback -} from 'react' -import axios from 'axios' +import React, { Dispatch, SetStateAction } from 'react' import { Backdrop, Box, - Button, CircularProgress, - FormControl, - IconButton, - Menu, - MenuItem, Paper, - Select, - SelectChangeEvent, Tab, Tooltip, Typography } from '@mui/material' import { styled } from '@mui/material/styles' -import { - RocketLaunch, - MoreVert, - Save, - SaveAs, - Difference, - Edit -} from '@mui/icons-material' -import Editor, { - MonacoDiffEditor, - DiffEditorDidMount, - EditorDidMount, - monaco -} from 'react-monaco-editor' +import Editor, { MonacoDiffEditor } from 'react-monaco-editor' import { TabContext, TabList, TabPanel } from '@mui/lab' -import { AppContext, RunTimeType } from '../../context/appContext' - import FilePathInputModal from '../../components/filePathInputModal' -import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar' -import Modal from '../../components/modal' +import FileMenu from './internal/components/fileMenu' +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)(() => ({ padding: '10px' @@ -70,267 +41,77 @@ type SASjsEditorProps = { setTab: Dispatch> } -const baseUrl = window.location.origin -const SASJS_LOGS_SEPARATOR = - 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784' - const SASjsEditor = ({ selectedFilePath, setSelectedFilePath, tab, setTab }: SASjsEditorProps) => { - const appContext = useContext(AppContext) - const [isLoading, setIsLoading] = useState(false) - const [openModal, setOpenModal] = useState(false) - const [modalTitle, setModalTitle] = useState('') - const [modalPayload, setModalPayload] = useState('') - const [openSnackbar, setOpenSnackbar] = useState(false) - const [snackbarMessage, setSnackbarMessage] = useState('') - const [snackbarSeverity, setSnackbarSeverity] = useState( - AlertSeverityType.Success - ) - 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([]) - 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 { + 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 + } = useEditor({ selectedFilePath, setSelectedFilePath, setTab }) usePrompt( 'Changes you made may not be saved.', prevFileContent !== fileContent && !!selectedFilePath ) - 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) - }) - }, - [ - fileContent, - prevFileContent, - selectedFilePath, - setPrevFileContent, - setSelectedFilePath - ] + const fileMenu = ( + ) - 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]) - - 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) => { - 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() || 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 monacoEditor = showDiff ? ( + setFileContent(val)} + /> + ) : ( + setFileContent(val)} + /> + ) return ( @@ -343,15 +124,7 @@ const SASjsEditor = ({ {selectedFilePath && !runTimes.includes(selectedFileExtension) ? ( - + {fileMenu} - {showDiff ? ( - setFileContent(val)} - /> - ) : ( - setFileContent(val)} - /> - )} + {monacoEditor} ) : ( @@ -419,15 +173,7 @@ const SASjsEditor = ({ handleChangeRunTime={handleChangeRunTime} handleRunBtnClick={handleRunBtnClick} /> - + {fileMenu} - {showDiff ? ( - setFileContent(val)} - /> - ) : ( - setFileContent(val)} - /> - )} + {monacoEditor}

)} - - +

+ void - handleRunBtnClick: () => void -} - -const RunMenu = ({ - selectedFilePath, - fileContent, - prevFileContent, - selectedRunTime, - runTimes, - handleChangeRunTime, - handleRunBtnClick -}: RunMenuProps) => { - const launchProgram = () => { - window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`) - } - - return ( - <> - - - - {selectedFilePath ? ( - - - - - - - - - - ) : ( - - - - - - )} - - ) -} - -type FileMenuProps = { - showDiff: boolean - setShowDiff: React.Dispatch> - prevFileContent: string - currentFileContent: string - selectedFilePath: string - setOpenFilePathInputModal: React.Dispatch> - 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 - ) => { - 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 ( - <> - - - - - - handleMenu()} - > - - - - - - - - - - - - ) -} - -const getLanguage = (extension: string) => { - if (extension === 'js') return 'javascript' - - if (extension === 'ts') return 'typescript' - - if (extension === 'md' || extension === 'mdx') return 'markdown' - - return extension -} diff --git a/web/src/containers/Studio/internal/components/fileMenu.tsx b/web/src/containers/Studio/internal/components/fileMenu.tsx new file mode 100644 index 0000000..d1fe317 --- /dev/null +++ b/web/src/containers/Studio/internal/components/fileMenu.tsx @@ -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> + prevFileContent: string + currentFileContent: string + selectedFilePath: string + setOpenFilePathInputModal: React.Dispatch> + 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 + ) => { + 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 ( + <> + + + + + + handleMenu()} + > + + + + + + + + + + + + ) +} + +export default FileMenu diff --git a/web/src/containers/Studio/internal/components/runMenu.tsx b/web/src/containers/Studio/internal/components/runMenu.tsx new file mode 100644 index 0000000..0ab05e9 --- /dev/null +++ b/web/src/containers/Studio/internal/components/runMenu.tsx @@ -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 ( + <> + + + + {selectedFilePath ? ( + + + + + + + + + + ) : ( + + + + + + )} + + ) +} + +export default RunMenu diff --git a/web/src/containers/Studio/internal/helper.ts b/web/src/containers/Studio/internal/helper.ts new file mode 100644 index 0000000..b368912 --- /dev/null +++ b/web/src/containers/Studio/internal/helper.ts @@ -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 ?? '' +} diff --git a/web/src/containers/Studio/internal/hooks/useEditor.ts b/web/src/containers/Studio/internal/hooks/useEditor.ts new file mode 100644 index 0000000..7cb8dd3 --- /dev/null +++ b/web/src/containers/Studio/internal/hooks/useEditor.ts @@ -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> +} + +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([]) + 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 diff --git a/web/src/context/permissionsContext.tsx b/web/src/context/permissionsContext.tsx new file mode 100644 index 0000000..e59f509 --- /dev/null +++ b/web/src/context/permissionsContext.tsx @@ -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> + permissions: PermissionResponse[] + setPermissions: Dispatch> + selectedPermission: PermissionResponse | undefined + setSelectedPermission: Dispatch< + React.SetStateAction + > + filteredPermissions: PermissionResponse[] + setFilteredPermissions: Dispatch> + filterApplied: boolean + setFilterApplied: Dispatch> + fetchPermissions: () => void + Dialog: () => JSX.Element + setOpenModal: Dispatch> + setModalTitle: Dispatch> + setModalPayload: Dispatch> + Snackbar: () => JSX.Element + setOpenSnackbar: Dispatch> + setSnackbarMessage: Dispatch> + setSnackbarSeverity: Dispatch> + PermissionResponseDialog: () => JSX.Element + setOpenPermissionResponseModal: Dispatch> + setPermissionResponsePayload: Dispatch< + React.SetStateAction + > +} + +export const PermissionsContext = createContext( + 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([]) + const [selectedPermission, setSelectedPermission] = + useState() + 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 ( + + {children} + + ) +} + +export default PermissionsContextProvider diff --git a/web/src/utils/hooks/index.ts b/web/src/utils/hooks/index.ts index bb694d1..48f7886 100644 --- a/web/src/utils/hooks/index.ts +++ b/web/src/utils/hooks/index.ts @@ -1,2 +1,4 @@ +export * from './useModal' export * from './usePrompt' export * from './useStateWithCallback' +export * from './useSnackbar' diff --git a/web/src/utils/hooks/useModal.tsx b/web/src/utils/hooks/useModal.tsx new file mode 100644 index 0000000..8553e2a --- /dev/null +++ b/web/src/utils/hooks/useModal.tsx @@ -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 = () => ( + + ) + + return { Dialog, setOpenModal, setModalTitle, setModalPayload } +} diff --git a/web/src/utils/hooks/useSnackbar.tsx b/web/src/utils/hooks/useSnackbar.tsx new file mode 100644 index 0000000..0fbc304 --- /dev/null +++ b/web/src/utils/hooks/useSnackbar.tsx @@ -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.Success + ) + + const Snackbar = () => ( + + ) + + return { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } +} From 16b7aa6abbbc97deb0989a86b6e7f2933ddec8e2 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 9 Sep 2022 00:49:26 +0500 Subject: [PATCH 4/5] chore: merge js, py and r session controller classes to base session controller class --- api/src/controllers/internal/Session.ts | 187 +++++------------------- api/src/routes/api/spec/stp.spec.ts | 11 +- api/src/types/system/process.d.ts | 5 +- 3 files changed, 41 insertions(+), 162 deletions(-) diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index a62a019..56e7df8 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -19,13 +19,41 @@ import { const execFilePromise = promisify(execFile) -abstract class SessionController { +export class SessionController { protected sessions: Session[] = [] protected getReadySessions = (): Session[] => this.sessions.filter((sess: Session) => sess.ready && !sess.consumed) - protected abstract createSession(): Promise + protected async createSession(): Promise { + 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() { const readySessions = this.getReadySessions() @@ -167,158 +195,17 @@ ${autoExecContent}` } } -export class JSSessionController extends SessionController { - protected async createSession(): Promise { - 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 { - 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 RSessionController extends SessionController { - protected async createSession(): Promise { - 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 = ( runTime: RunTimeType -): - | SASSessionController - | JSSessionController - | PythonSessionController - | RSessionController => { - if (runTime === RunTimeType.SAS) { - return getSASSessionController() - } +): SessionController => { + if (process.sessionController) return process.sessionController - if (runTime === RunTimeType.JS) { - return getJSSessionController() - } + process.sessionController = + runTime === RunTimeType.SAS + ? new SASSessionController() + : new SessionController() - if (runTime === RunTimeType.PY) { - return getPythonSessionController() - } - - if (runTime === RunTimeType.R) { - return getRSessionController() - } - - 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 getRSessionController = (): RSessionController => { - if (process.rSessionController) return process.rSessionController - - process.rSessionController = new RSessionController() - - return process.rSessionController + return process.sessionController } const autoExecContent = ` diff --git a/api/src/routes/api/spec/stp.spec.ts b/api/src/routes/api/spec/stp.spec.ts index 7304a4f..1512378 100644 --- a/api/src/routes/api/spec/stp.spec.ts +++ b/api/src/routes/api/spec/stp.spec.ts @@ -21,9 +21,8 @@ import { } from '../../../utils' import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils' import { - SASSessionController, - JSSessionController, - PythonSessionController + SessionController, + SASSessionController } from '../../../controllers/internal' import * as ProcessProgramModule from '../../../controllers/internal/processProgram' import { Session } from '../../../types' @@ -472,11 +471,7 @@ const setupMocks = async () => { .mockImplementation(mockedGetSession) jest - .spyOn(JSSessionController.prototype, 'getSession') - .mockImplementation(mockedGetSession) - - jest - .spyOn(PythonSessionController.prototype, 'getSession') + .spyOn(SASSessionController.prototype, 'getSession') .mockImplementation(mockedGetSession) jest diff --git a/api/src/types/system/process.d.ts b/api/src/types/system/process.d.ts index 4be0801..2090755 100644 --- a/api/src/types/system/process.d.ts +++ b/api/src/types/system/process.d.ts @@ -7,10 +7,7 @@ declare namespace NodeJS { driveLoc: string logsLoc: string logsUUID: string - sasSessionController?: import('../../controllers/internal').SASSessionController - jsSessionController?: import('../../controllers/internal').JSSessionController - pythonSessionController?: import('../../controllers/internal').PythonSessionController - rSessionController?: import('../../controllers/internal').RSessionController + sessionController?: import('../../controllers/internal').SessionController appStreamConfig: import('../').AppStreamConfig logger: import('@sasjs/utils/logger').Logger runTimes: import('../../utils').RunTimeType[] From 662b2ca36a11726fb06765412b0db60a668b5606 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Fri, 9 Sep 2022 15:23:46 +0500 Subject: [PATCH 5/5] chore: replace env variable RSCRIPT_PATH with R_PATH --- README.md | 6 +++--- api/.env.example | 2 +- api/src/controllers/internal/processProgram.ts | 2 +- api/src/types/system/process.d.ts | 2 +- api/src/utils/getDesktopFields.ts | 14 +++++++------- api/src/utils/setProcessVariables.ts | 6 +++--- api/src/utils/verifyEnvVariables.ts | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3d7aa61..b67ba11 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ MODE= # A comma separated string that defines the available runTimes. # 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 # Valid runtimes are SAS (sas), JavaScript (js), Python (py) and R (r) @@ -85,8 +85,8 @@ NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node # Path to Python executable PYTHON_PATH=/usr/bin/python -# Path to Rscript -RSCRIPT_PATH=/usr/bin/Rscript +# Path to R executable +R_PATH=/usr/bin/Rscript # Path to working directory # This location is for SAS WORK, staged files, DRIVE, configuration etc diff --git a/api/.env.example b/api/.env.example index 7c375db..7d509f3 100644 --- a/api/.env.example +++ b/api/.env.example @@ -18,7 +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 NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node PYTHON_PATH=/usr/bin/python -RSCRIPT_PATH=/usr/bin/Rscript +R_PATH=/usr/bin/Rscript SASJS_ROOT=./sasjs_root diff --git a/api/src/controllers/internal/processProgram.ts b/api/src/controllers/internal/processProgram.ts index 6bd017c..cd7966c 100644 --- a/api/src/controllers/internal/processProgram.ts +++ b/api/src/controllers/internal/processProgram.ts @@ -97,7 +97,7 @@ export const processProgram = async ( otherArgs ) codePath = path.join(session.path, 'code.r') - executablePath = process.rscriptLoc! + executablePath = process.rLoc! break default: diff --git a/api/src/types/system/process.d.ts b/api/src/types/system/process.d.ts index 2090755..1c2077d 100644 --- a/api/src/types/system/process.d.ts +++ b/api/src/types/system/process.d.ts @@ -3,7 +3,7 @@ declare namespace NodeJS { sasLoc?: string nodeLoc?: string pythonLoc?: string - rscriptLoc?: string + rLoc?: string driveLoc: string logsLoc: string logsUUID: string diff --git a/api/src/utils/getDesktopFields.ts b/api/src/utils/getDesktopFields.ts index fe2604a..16dff05 100644 --- a/api/src/utils/getDesktopFields.ts +++ b/api/src/utils/getDesktopFields.ts @@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils' import { RunTimeType } from './verifyEnvVariables' export const getDesktopFields = async () => { - const { SAS_PATH, NODE_PATH, PYTHON_PATH, RSCRIPT_PATH } = process.env + const { SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH } = process.env - let sasLoc, nodeLoc, pythonLoc, rscriptLoc + let sasLoc, nodeLoc, pythonLoc, rLoc if (process.runTimes.includes(RunTimeType.SAS)) { sasLoc = SAS_PATH ?? (await getSASLocation()) @@ -21,10 +21,10 @@ export const getDesktopFields = async () => { } if (process.runTimes.includes(RunTimeType.R)) { - rscriptLoc = RSCRIPT_PATH ?? (await getRScriptLocation()) + rLoc = R_PATH ?? (await getRLocation()) } - return { sasLoc, nodeLoc, pythonLoc, rscriptLoc } + return { sasLoc, nodeLoc, pythonLoc, rLoc } } const getDriveLocation = async (): Promise => { @@ -122,9 +122,9 @@ const getPythonLocation = async (): Promise => { return targetName } -const getRScriptLocation = async (): Promise => { +const getRLocation = async (): Promise => { const validator = async (filePath: string) => { - if (!filePath) return 'Path to RScript executable is required.' + if (!filePath) return 'Path to R executable is required.' if (!(await fileExists(filePath))) { return 'No file found at provided path.' @@ -136,7 +136,7 @@ const getRScriptLocation = async (): Promise => { const defaultLocation = isWindows() ? 'C:\\Rscript' : '/usr/bin/Rscript' const targetName = await getString( - 'Please enter full path to a Rscript executable: ', + 'Please enter full path to a R executable: ', validator, defaultLocation ) diff --git a/api/src/utils/setProcessVariables.ts b/api/src/utils/setProcessVariables.ts index 8192888..395e5fc 100644 --- a/api/src/utils/setProcessVariables.ts +++ b/api/src/utils/setProcessVariables.ts @@ -29,14 +29,14 @@ export const setProcessVariables = async () => { process.sasLoc = process.env.SAS_PATH process.nodeLoc = process.env.NODE_PATH process.pythonLoc = process.env.PYTHON_PATH - process.rscriptLoc = process.env.RSCRIPT_PATH + process.rLoc = process.env.R_PATH } else { - const { sasLoc, nodeLoc, pythonLoc, rscriptLoc } = await getDesktopFields() + const { sasLoc, nodeLoc, pythonLoc, rLoc } = await getDesktopFields() process.sasLoc = sasLoc process.nodeLoc = nodeLoc process.pythonLoc = pythonLoc - process.rscriptLoc = rscriptLoc + process.rLoc = rLoc } const { SASJS_ROOT } = process.env diff --git a/api/src/utils/verifyEnvVariables.ts b/api/src/utils/verifyEnvVariables.ts index 65f3cdb..9c75852 100644 --- a/api/src/utils/verifyEnvVariables.ts +++ b/api/src/utils/verifyEnvVariables.ts @@ -254,7 +254,7 @@ const verifyRUN_TIMES = (): string[] => { const verifyExecutablePaths = () => { const errors: string[] = [] - const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, RSCRIPT_PATH, MODE } = + const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, R_PATH, MODE } = process.env if (MODE === ModeType.Server) { @@ -272,8 +272,8 @@ const verifyExecutablePaths = () => { errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`) } - if (runTimes?.includes(RunTimeType.R) && !RSCRIPT_PATH) { - errors.push(`- RSCRIPT_PATH is required for ${RunTimeType.R} run time`) + if (runTimes?.includes(RunTimeType.R) && !R_PATH) { + errors.push(`- R_PATH is required for ${RunTimeType.R} run time`) } }