diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 65b8c5a..18e34df 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -92,6 +92,16 @@ components: - clientSecret type: object additionalProperties: false + ExecuteSASCodePayload: + properties: + code: + type: string + description: 'Code of SAS program' + example: '* SAS Code HERE;' + required: + - code + type: object + additionalProperties: false MemberType.folder: enum: - folder @@ -358,16 +368,6 @@ components: - description type: object additionalProperties: false - RunSASPayload: - properties: - code: - type: string - description: 'Code of SAS program' - example: '* SAS Code HERE;' - required: - - code - type: object - additionalProperties: false ExecuteReturnJsonResponse: properties: status: @@ -511,6 +511,30 @@ paths: application/json: schema: $ref: '#/components/schemas/ClientPayload' + /SASjsApi/code/execute: + post: + operationId: ExecuteSASCode + responses: + '200': + description: Ok + content: + application/json: + schema: + type: string + description: 'Execute SAS code.' + summary: 'Run SAS Code and returns log' + tags: + - CODE + security: + - + bearerAuth: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExecuteSASCodePayload' /SASjsApi/drive/deploy: post: operationId: Deploy @@ -982,6 +1006,26 @@ paths: format: double type: number example: '6789' + /SASjsApi/session: + get: + operationId: Session + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + examples: + 'Example 1': + value: {id: 123, username: johnusername, displayName: John} + summary: 'Get session info (username).' + tags: + - Session + security: + - + bearerAuth: [] + parameters: [] /SASjsApi/stp/execute: get: operationId: ExecuteReturnRaw @@ -1037,50 +1081,6 @@ paths: application/json: schema: $ref: '#/components/schemas/ExecuteReturnJsonPayload' - /SASjsApi/stp/run: - post: - operationId: RunSAS - responses: - '200': - description: Ok - content: - application/json: - schema: - type: string - description: 'Trigger a SAS program.' - summary: 'Run SAS Program, return raw content' - tags: - - STP - security: - - - bearerAuth: [] - parameters: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RunSASPayload' - /SASjsApi/session: - get: - operationId: Session - responses: - '200': - description: Ok - content: - application/json: - schema: - $ref: '#/components/schemas/UserResponse' - examples: - 'Example 1': - value: {id: 123, username: johnusername, displayName: John} - summary: 'Get session info (username).' - tags: - - Session - security: - - - bearerAuth: [] - parameters: [] servers: - url: / @@ -1106,3 +1106,6 @@ tags: - name: STP description: 'Operations about STP' + - + name: CODE + description: 'Operations on SAS code' diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts new file mode 100644 index 0000000..52e3266 --- /dev/null +++ b/api/src/controllers/code.ts @@ -0,0 +1,63 @@ +import express from 'express' +import { Request, Security, Route, Tags, Post, Body } from 'tsoa' +import { ExecutionController } from './internal' +import { PreProgramVars } from '../types' + +interface ExecuteSASCodePayload { + /** + * Code of SAS program + * @example "* SAS Code HERE;" + */ + code: string +} + +@Security('bearerAuth') +@Route('SASjsApi/code') +@Tags('CODE') +export class CodeController { + /** + * Execute SAS code. + * @summary Run SAS Code and returns log + */ + @Post('/execute') + public async executeSASCode( + @Request() request: express.Request, + @Body() body: ExecuteSASCodePayload + ): Promise { + return executeSASCode(request, body) + } +} + +const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => { + try { + const result = await new ExecutionController().executeProgram( + code, + getPreProgramVariables(req), + { ...req.query, _debug: 131 }, + undefined, + true + ) + + return result as string + } catch (err: any) { + throw { + code: 400, + status: 'failure', + message: 'Job execution failed.', + error: typeof err === 'object' ? err.toString() : err + } + } +} + +const getPreProgramVariables = (req: any): PreProgramVars => { + const host = req.get('host') + const protocol = req.protocol + '://' + const { user, accessToken } = req + return { + username: user.username, + userId: user.userId, + displayName: user.displayName, + serverUrl: protocol + host, + accessToken + } +} diff --git a/api/src/controllers/index.ts b/api/src/controllers/index.ts index 805cc3c..d2efa88 100644 --- a/api/src/controllers/index.ts +++ b/api/src/controllers/index.ts @@ -1,7 +1,8 @@ export * from './auth' export * from './client' +export * from './code' export * from './drive' export * from './group' +export * from './session' export * from './stp' export * from './user' -export * from './session' diff --git a/api/src/controllers/internal/Session.ts b/api/src/controllers/internal/Session.ts index 38df221..e9bf581 100644 --- a/api/src/controllers/internal/Session.ts +++ b/api/src/controllers/internal/Session.ts @@ -110,7 +110,7 @@ export class SessionController { // TODO: don't wait forever while ((await fileExists(codeFilePath)) && !session.crashed) {} - console.log('session crashed?', !!session.crashed, session.crashed) + console.log('session crashed?', !!session.crashed, session.crashed || '') session.ready = true return Promise.resolve(session) diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index d94ac25..b0f78ac 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -5,13 +5,6 @@ import { ExecutionController } from './internal' import { PreProgramVars } from '../types' import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils' -interface RunSASPayload { - /** - * Code of SAS program - * @example "* SAS Code HERE;" - */ - code: string -} interface ExecuteReturnJsonPayload { /** * Location of SAS program @@ -48,18 +41,6 @@ export class STPController { return executeReturnRaw(request, _program) } - /** - * Trigger a SAS program. - * @summary Run SAS Program, return raw content - */ - @Post('/run') - public async runSAS( - @Request() request: express.Request, - @Body() body: RunSASPayload - ): Promise { - return runSAS(request, body) - } - /** * Trigger a SAS program using it's location in the _program parameter. * Enable debugging using the _debug parameter. @@ -109,25 +90,6 @@ const executeReturnRaw = async ( } } -const runSAS = async (req: any, { code }: RunSASPayload) => { - try { - const result = await new ExecutionController().executeProgram( - code, - getPreProgramVariables(req), - req.query - ) - - return result as string - } catch (err: any) { - throw { - code: 400, - status: 'failure', - message: 'Job execution failed.', - error: typeof err === 'object' ? err.toString() : err - } - } -} - const executeReturnJson = async ( req: any, _program: string diff --git a/api/src/routes/api/code.ts b/api/src/routes/api/code.ts new file mode 100644 index 0000000..fb751b5 --- /dev/null +++ b/api/src/routes/api/code.ts @@ -0,0 +1,25 @@ +import express from 'express' +import { runSASValidation } from '../../utils' +import { CodeController } from '../../controllers/' + +const runRouter = express.Router() + +const controller = new CodeController() + +runRouter.post('/execute', async (req, res) => { + const { error, value: body } = runSASValidation(req.body) + if (error) return res.status(400).send(error.details[0].message) + + try { + const response = await controller.executeSASCode(req, body) + res.send(response) + } catch (err: any) { + const statusCode = err.code + + delete err.code + + res.status(statusCode).send(err) + } +}) + +export default runRouter diff --git a/api/src/routes/api/index.ts b/api/src/routes/api/index.ts index b91715c..e21022d 100644 --- a/api/src/routes/api/index.ts +++ b/api/src/routes/api/index.ts @@ -11,6 +11,7 @@ import { import driveRouter from './drive' import stpRouter from './stp' +import codeRouter from './code' import userRouter from './user' import groupRouter from './group' import clientRouter from './client' @@ -31,6 +32,7 @@ router.use( router.use('/drive', authenticateAccessToken, driveRouter) router.use('/group', desktopRestrict, groupRouter) router.use('/stp', authenticateAccessToken, stpRouter) +router.use('/code', authenticateAccessToken, codeRouter) router.use('/user', desktopRestrict, userRouter) router.use( '/', diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index b00707d..1ca4a18 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -24,22 +24,6 @@ stpRouter.get('/execute', async (req, res) => { } }) -stpRouter.post('/run', async (req, res) => { - const { error, value: body } = runSASValidation(req.body) - if (error) return res.status(400).send(error.details[0].message) - - try { - const response = await controller.runSAS(req, body) - res.send(response) - } catch (err: any) { - const statusCode = err.code - - delete err.code - - res.status(statusCode).send(err) - } -}) - stpRouter.post( '/execute', fileUploadController.preuploadMiddleware, diff --git a/api/tsoa.json b/api/tsoa.json index b1b912b..8fe396f 100644 --- a/api/tsoa.json +++ b/api/tsoa.json @@ -38,6 +38,10 @@ { "name": "STP", "description": "Operations about STP" + }, + { + "name": "CODE", + "description": "Operations on SAS code" } ], "yaml": true, diff --git a/docker-compose.yml b/docker-compose.yml index f7368c7..7bce991 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,7 +43,7 @@ services: - ./web:/usr/server/web mongodb: - image: mongo:latest + image: mongo:5.0.4 ports: - 27017:27017 volumes: diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index 0e6d87a..7174d2e 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -2,17 +2,37 @@ import React, { useEffect, useRef, useState } from 'react' import axios from 'axios' import Box from '@mui/material/Box' -import { Button, Paper, Stack, Toolbar } from '@mui/material' -import Editor from '@monaco-editor/react' +import { Button, Paper, Stack, Tab } from '@mui/material' +import { makeStyles } from '@mui/styles' +import Editor, { OnMount } from '@monaco-editor/react' import { useLocation } from 'react-router-dom' +import { TabContext, TabList, TabPanel } from '@mui/lab' + +const useStyles = makeStyles(() => ({ + root: { + fontSize: '1rem', + color: 'gray', + '&.Mui-selected': { + color: 'black' + } + } +})) const Studio = () => { const location = useLocation() const [fileContent, setFileContent] = useState('') const [log, setLog] = useState('') + const [webout, setWebout] = useState('') + const [tab, setTab] = React.useState('1') + const handleTabChange = (_e: any, newValue: string) => { + setTab(newValue) + } - const editorRef = useRef(null) - const handleEditorDidMount = (editor: any) => (editorRef.current = editor) + const editorRef = useRef(null as any) + const handleEditorDidMount: OnMount = (editor) => { + editor.focus() + editorRef.current = editor + } const getSelection = () => { const editor = editorRef.current as any @@ -20,25 +40,47 @@ const Studio = () => { return selection ?? '' } - const handleRunSelectionBtnClick = () => runCode(getSelection()) - - const handleRunBtnClick = () => runCode(fileContent) + const handleRunBtnClick = () => runCode(getSelection() || fileContent) const runCode = (code: string) => { axios - .post(`/SASjsApi/stp/run`, { code }) + .post(`/SASjsApi/code/execute`, { code }) .then((res: any) => { - const data = - typeof res.data === 'string' - ? res.data - : `
${JSON.stringify(res.data, null, 4)}
` + setLog(`

SAS Log

${res?.data?.log}
`) - setLog(data) - document?.getElementById('sas_log')?.scrollIntoView() + let weboutString: string + try { + weboutString = res.data.webout + .split('>>weboutBEGIN<<')[1] + .split('>>weboutEND<<')[0] + } catch (_) { + weboutString = res?.data?.webout ?? '' + } + + let webout: string + try { + webout = JSON.stringify(JSON.parse(weboutString), null, 4) + } catch (_) { + webout = weboutString + } + + setWebout(`
${webout}
`) + setTab('2') }) .catch((err) => console.log(err)) } + useEffect(() => { + const content = localStorage.getItem('fileContent') ?? '' + setFileContent(content) + }, []) + + useEffect(() => { + if (fileContent.length) { + localStorage.setItem('fileContent', fileContent) + } + }, [fileContent]) + useEffect(() => { const params = new URLSearchParams(location.search) const programPath = params.get('_program') @@ -50,48 +92,74 @@ const Studio = () => { .catch((err) => console.log(err)) }, [location.search]) + const classes = useStyles() return ( - - - - { - if (val) setFileContent(val) - }} - /> - - - - - - {log && ( - <> -
-

Output

-
-
- - )} - + <> +
+
+
+ + + + + + + + + + + {/* */} + + { + if (val) setFileContent(val) + }} + /> + + + + + + +
+ + +
+ + + + ) }