mirror of
https://github.com/sasjs/server.git
synced 2026-01-10 07:50:05 +00:00
feat(studio): run selected code + open in studio
This commit is contained in:
@@ -358,6 +358,16 @@ components:
|
|||||||
- description
|
- description
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
RunSASPayload:
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
description: 'Code of SAS program'
|
||||||
|
example: '* SAS Code HERE;'
|
||||||
|
required:
|
||||||
|
- code
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
ExecuteReturnJsonResponse:
|
ExecuteReturnJsonResponse:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
@@ -1027,6 +1037,30 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
|
$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:
|
/SASjsApi/session:
|
||||||
get:
|
get:
|
||||||
operationId: Session
|
operationId: Session
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { PreProgramVars, TreeNode } from '../../types'
|
|||||||
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
|
||||||
|
|
||||||
export class ExecutionController {
|
export class ExecutionController {
|
||||||
async execute(
|
async executeFile(
|
||||||
programPath: string,
|
programPath: string,
|
||||||
preProgramVariables: PreProgramVars,
|
preProgramVariables: PreProgramVars,
|
||||||
vars: { [key: string]: string | number | undefined },
|
vars: { [key: string]: string | number | undefined },
|
||||||
@@ -16,8 +16,23 @@ export class ExecutionController {
|
|||||||
if (!(await fileExists(programPath)))
|
if (!(await fileExists(programPath)))
|
||||||
throw 'ExecutionController: SAS file does not exist.'
|
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 sessionController = getSessionController()
|
||||||
|
|
||||||
const session = await sessionController.getSession()
|
const session = await sessionController.getSession()
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ import { ExecutionController } from './internal'
|
|||||||
import { PreProgramVars } from '../types'
|
import { PreProgramVars } from '../types'
|
||||||
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
|
||||||
|
|
||||||
|
interface RunSASPayload {
|
||||||
|
/**
|
||||||
|
* Code of SAS program
|
||||||
|
* @example "* SAS Code HERE;"
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
|
}
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecuteReturnJsonPayload {
|
||||||
/**
|
/**
|
||||||
* Location of SAS program
|
* Location of SAS program
|
||||||
@@ -40,6 +47,19 @@ export class STPController {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
return executeReturnRaw(request, _program)
|
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<string> {
|
||||||
|
return runSAS(request, body)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a SAS program using it's location in the _program parameter.
|
* Trigger a SAS program using it's location in the _program parameter.
|
||||||
* Enable debugging using the _debug parameter.
|
* Enable debugging using the _debug parameter.
|
||||||
@@ -72,7 +92,7 @@ const executeReturnRaw = async (
|
|||||||
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await new ExecutionController().execute(
|
const result = await new ExecutionController().executeFile(
|
||||||
sasCodePath,
|
sasCodePath,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
query
|
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 (
|
const executeReturnJson = async (
|
||||||
req: any,
|
req: any,
|
||||||
_program: string
|
_program: string
|
||||||
@@ -101,7 +140,7 @@ const executeReturnJson = async (
|
|||||||
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log } = (await new ExecutionController().execute(
|
const { webout, log } = (await new ExecutionController().executeFile(
|
||||||
sasCodePath,
|
sasCodePath,
|
||||||
getPreProgramVariables(req),
|
getPreProgramVariables(req),
|
||||||
{ ...req.query, ...req.body },
|
{ ...req.query, ...req.body },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { executeProgramRawValidation } from '../../utils'
|
import { executeProgramRawValidation, runSASValidation } from '../../utils'
|
||||||
import { STPController } from '../../controllers/'
|
import { STPController } from '../../controllers/'
|
||||||
import { FileUploadController } from '../../controllers/internal'
|
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(
|
stpRouter.post(
|
||||||
'/execute',
|
'/execute',
|
||||||
fileUploadController.preuploadMiddleware,
|
fileUploadController.preuploadMiddleware,
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
|
|||||||
fileContent: Joi.string().required()
|
fileContent: Joi.string().required()
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
|
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||||
|
Joi.object({
|
||||||
|
code: Joi.string().required()
|
||||||
|
}).validate(data)
|
||||||
|
|
||||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||||
Joi.object({
|
Joi.object({
|
||||||
_program: Joi.string().required()
|
_program: Joi.string().required()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import Editor from '@monaco-editor/react'
|
import Editor from '@monaco-editor/react'
|
||||||
@@ -89,10 +90,10 @@ const Main = (props: any) => {
|
|||||||
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
style={{ position: 'absolute', left: '50%', top: '50%' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && props?.selectedFilePath !== '' && !editMode && (
|
{!isLoading && props?.selectedFilePath && !editMode && (
|
||||||
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
|
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
|
||||||
)}
|
)}
|
||||||
{!isLoading && props?.selectedFilePath !== '' && editMode && (
|
{!isLoading && props?.selectedFilePath && editMode && (
|
||||||
<Editor
|
<Editor
|
||||||
height="95%"
|
height="95%"
|
||||||
value={fileContent}
|
value={fileContent}
|
||||||
@@ -110,17 +111,26 @@ const Main = (props: any) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleEditSaveBtnClick}
|
onClick={handleEditSaveBtnClick}
|
||||||
disabled={isLoading || props?.selectedFilePath === ''}
|
disabled={isLoading || !props?.selectedFilePath}
|
||||||
>
|
>
|
||||||
{!editMode ? 'Edit' : 'Save'}
|
{!editMode ? 'Edit' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleCancelExecuteBtnClick}
|
onClick={handleCancelExecuteBtnClick}
|
||||||
disabled={isLoading || props?.selectedFilePath === ''}
|
disabled={isLoading || !props?.selectedFilePath}
|
||||||
>
|
>
|
||||||
{editMode ? 'Cancel' : 'Execute'}
|
{editMode ? 'Cancel' : 'Execute'}
|
||||||
</Button>
|
</Button>
|
||||||
|
{props?.selectedFilePath && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
component={Link}
|
||||||
|
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
|
||||||
|
>
|
||||||
|
Open in Studio
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 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 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 (
|
return (
|
||||||
<Box className="main">
|
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||||
<CssBaseline />
|
<Toolbar />
|
||||||
<h2>This is container for SASjs studio</h2>
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: '75vh',
|
||||||
|
padding: '10px',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
elevation={3}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height="95%"
|
||||||
|
value={fileContent}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (val) setFileContent(val)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
<Stack
|
||||||
|
spacing={3}
|
||||||
|
direction="row"
|
||||||
|
sx={{ justifyContent: 'center', marginTop: '20px' }}
|
||||||
|
>
|
||||||
|
<Button variant="contained" onClick={handleRunBtnClick}>
|
||||||
|
Run SAS Code
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleRunSelectionBtnClick}>
|
||||||
|
Run Selected SAS Code
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
{log && <div id="sas_log" dangerouslySetInnerHTML={{ __html: log }} />}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user