diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index eb7c25d..65b8c5a 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -358,6 +358,16 @@ 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: @@ -1027,6 +1037,30 @@ 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 diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 6738142..5abb742 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -6,7 +6,7 @@ import { PreProgramVars, TreeNode } from '../../types' import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils' export class ExecutionController { - async execute( + async executeFile( programPath: string, preProgramVariables: PreProgramVars, vars: { [key: string]: string | number | undefined }, @@ -16,8 +16,23 @@ export class ExecutionController { if (!(await fileExists(programPath))) throw 'ExecutionController: SAS file does not exist.' - let program = await readFile(programPath) + const program = await readFile(programPath) + return this.executeProgram( + program, + preProgramVariables, + vars, + otherArgs, + returnJson + ) + } + async executeProgram( + program: string, + preProgramVariables: PreProgramVars, + vars: { [key: string]: string | number | undefined }, + otherArgs?: any, + returnJson?: boolean + ) { const sessionController = getSessionController() const session = await sessionController.getSession() diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index eb90c9d..d94ac25 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -5,6 +5,13 @@ 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 @@ -40,6 +47,19 @@ export class STPController { ): Promise { 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. @@ -72,7 +92,7 @@ const executeReturnRaw = async ( .replace(new RegExp('/', 'g'), path.sep) + '.sas' try { - const result = await new ExecutionController().execute( + const result = await new ExecutionController().executeFile( sasCodePath, getPreProgramVariables(req), query @@ -89,6 +109,25 @@ 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 @@ -101,7 +140,7 @@ const executeReturnJson = async ( const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null try { - const { webout, log } = (await new ExecutionController().execute( + const { webout, log } = (await new ExecutionController().executeFile( sasCodePath, getPreProgramVariables(req), { ...req.query, ...req.body }, diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index 2564f11..b00707d 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -1,5 +1,5 @@ import express from 'express' -import { executeProgramRawValidation } from '../../utils' +import { executeProgramRawValidation, runSASValidation } from '../../utils' import { STPController } from '../../controllers/' import { FileUploadController } from '../../controllers/internal' @@ -24,6 +24,22 @@ 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/src/utils/validation.ts b/api/src/utils/validation.ts index 266e723..3efd5a9 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -77,6 +77,11 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult => fileContent: Joi.string().required() }).validate(data) +export const runSASValidation = (data: any): Joi.ValidationResult => + Joi.object({ + code: Joi.string().required() + }).validate(data) + export const executeProgramRawValidation = (data: any): Joi.ValidationResult => Joi.object({ _program: Joi.string().required() diff --git a/web/src/containers/Drive/main.tsx b/web/src/containers/Drive/main.tsx index d33e2cc..1e40cbb 100644 --- a/web/src/containers/Drive/main.tsx +++ b/web/src/containers/Drive/main.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' import axios from 'axios' import Editor from '@monaco-editor/react' @@ -89,10 +90,10 @@ const Main = (props: any) => { style={{ position: 'absolute', left: '50%', top: '50%' }} /> )} - {!isLoading && props?.selectedFilePath !== '' && !editMode && ( + {!isLoading && props?.selectedFilePath && !editMode && ( {fileContent} )} - {!isLoading && props?.selectedFilePath !== '' && editMode && ( + {!isLoading && props?.selectedFilePath && editMode && ( { + {props?.selectedFilePath && ( + + )} ) diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index 1fa94f4..145c786 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -1,13 +1,84 @@ -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' +import axios from 'axios' -import CssBaseline from '@mui/material/CssBaseline' import Box from '@mui/material/Box' +import { Button, Paper, Stack, Toolbar } from '@mui/material' +import Editor from '@monaco-editor/react' +import { useLocation } from 'react-router-dom' const Studio = () => { + const location = useLocation() + const [fileContent, setFileContent] = useState('') + const [log, setLog] = useState('') + + const editorRef = useRef(null) + const handleEditorDidMount = (editor: any) => (editorRef.current = editor) + + const getSelection = () => { + const editor = editorRef.current as any + const selection = editor?.getModel().getValueInRange(editor?.getSelection()) + return selection ?? '' + } + + const handleRunSelectionBtnClick = () => runCode(getSelection()) + + const handleRunBtnClick = () => runCode(fileContent) + + const runCode = (code: string) => { + axios + .post(`/SASjsApi/stp/run`, { code }) + .then((res: any) => { + setLog(res.data) + document?.getElementById('sas_log')?.scrollIntoView() + }) + .catch((err) => console.log(err)) + } + + useEffect(() => { + const params = new URLSearchParams(location.search) + const programPath = params.get('_program') + + if (programPath?.length) + axios + .get(`/SASjsApi/drive/file?filePath=${programPath}`) + .then((res: any) => setFileContent(res.data.fileContent)) + .catch((err) => console.log(err)) + }, [location.search]) + return ( - - -

This is container for SASjs studio

+ + + + { + if (val) setFileContent(val) + }} + /> + + + + + + {log &&
} ) }