mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 19:44:35 +00:00
feat: add sidebar(drive) to left of studio
This commit is contained in:
519
web/src/containers/Studio/editor.tsx
Normal file
519
web/src/containers/Studio/editor.tsx
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
import React, { useEffect, useRef, useState, useContext } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Backdrop,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Tab,
|
||||||
|
Tooltip
|
||||||
|
} from '@mui/material'
|
||||||
|
import { styled } from '@mui/material/styles'
|
||||||
|
|
||||||
|
import { RocketLaunch, MoreVert, Save, SaveAs } from '@mui/icons-material'
|
||||||
|
import Editor, { EditorDidMount } 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'
|
||||||
|
|
||||||
|
const StyledTabPanel = styled(TabPanel)(() => ({
|
||||||
|
padding: '10px'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const StyledTab = styled(Tab)(() => ({
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'gray',
|
||||||
|
'&.Mui-selected': {
|
||||||
|
color: 'black'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
type SASjsEditorProps = {
|
||||||
|
selectedFilePath: string
|
||||||
|
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = window.location.origin
|
||||||
|
|
||||||
|
const SASjsEditor = ({
|
||||||
|
selectedFilePath,
|
||||||
|
setSelectedFilePath
|
||||||
|
}: 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>(
|
||||||
|
AlertSeverityType.Success
|
||||||
|
)
|
||||||
|
const [prevFileContent, setPrevFileContent] = useState('')
|
||||||
|
const [fileContent, setFileContent] = useState('')
|
||||||
|
const [log, setLog] = useState('')
|
||||||
|
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||||
|
const [webout, setWebout] = useState('')
|
||||||
|
const [tab, setTab] = useState('1')
|
||||||
|
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||||
|
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||||
|
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||||
|
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||||
|
|
||||||
|
const editorRef = useRef(null as any)
|
||||||
|
|
||||||
|
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||||
|
editor.focus()
|
||||||
|
editorRef.current = editor
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}, [selectedFilePath])
|
||||||
|
|
||||||
|
const handleTabChange = (_e: any, newValue: string) => {
|
||||||
|
setTab(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelection = () => {
|
||||||
|
const editor = editorRef.current as any
|
||||||
|
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||||
|
return selection ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
|
||||||
|
|
||||||
|
const runCode = (code: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||||
|
.then((res: any) => {
|
||||||
|
const parsedLog = res?.data?.log
|
||||||
|
.map((logLine: any) => logLine.line)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
setLog(parsedLog)
|
||||||
|
|
||||||
|
setWebout(`${res.data?._webout}`)
|
||||||
|
setTab('2')
|
||||||
|
|
||||||
|
// Scroll to bottom of log
|
||||||
|
window.scrollTo(0, document.body.scrollHeight)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: any) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
if (event.key === 'v') {
|
||||||
|
setCtrlPressed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter') runCode(getSelection() || fileContent)
|
||||||
|
if (!ctrlPressed) setCtrlPressed(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyUp = (event: any) => {
|
||||||
|
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChangeRunTime = (event: SelectChangeEvent) => {
|
||||||
|
setSelectedRunTime(event.target.value as RunTimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilePathInput = (filePath: string) => {
|
||||||
|
setOpenFilePathInputModal(false)
|
||||||
|
saveFile(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveFile = (filePath?: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob, 'filename.sas')
|
||||||
|
formData.append('filePath', filePath ?? selectedFilePath)
|
||||||
|
|
||||||
|
const axiosPromise = filePath
|
||||||
|
? axios.post('/SASjsApi/drive/file', formData)
|
||||||
|
: axios.patch('/SASjsApi/drive/file', formData)
|
||||||
|
|
||||||
|
axiosPromise
|
||||||
|
.then(() => {
|
||||||
|
if (filePath) {
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
}
|
||||||
|
setPrevFileContent(fileContent)
|
||||||
|
setSnackbarMessage('File saved!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
||||||
|
>
|
||||||
|
<Backdrop
|
||||||
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
|
open={isLoading}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</Backdrop>
|
||||||
|
{selectedFilePath && !runTimes.includes(selectedFileExtension) ? (
|
||||||
|
<Box sx={{ marginTop: '10px' }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<FileMenu
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
|
currentFileContent={fileContent}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||||
|
saveFile={saveFile}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100vh - 140px)',
|
||||||
|
padding: '10px',
|
||||||
|
margin: '0 24px',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
elevation={3}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height="98%"
|
||||||
|
language="sas"
|
||||||
|
value={fileContent}
|
||||||
|
editorDidMount={handleEditorDidMount}
|
||||||
|
options={{ readOnly: ctrlPressed }}
|
||||||
|
onChange={(val) => setFileContent(val)}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<TabContext value={tab}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: 'divider',
|
||||||
|
position: 'fixed',
|
||||||
|
background: 'white',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabList onChange={handleTabChange} centered>
|
||||||
|
<StyledTab label="Code" value="1" />
|
||||||
|
<StyledTab label="Log" value="2" />
|
||||||
|
<Tooltip title="Displays content from the _webout fileref">
|
||||||
|
<StyledTab label="Webout" value="3" />
|
||||||
|
</Tooltip>
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<StyledTabPanel
|
||||||
|
sx={{ paddingBottom: 0, marginTop: '45px' }}
|
||||||
|
value="1"
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<RunMenu
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
selectedRunTime={selectedRunTime}
|
||||||
|
runTimes={runTimes}
|
||||||
|
handleChangeRunTime={handleChangeRunTime}
|
||||||
|
handleRunBtnClick={handleRunBtnClick}
|
||||||
|
/>
|
||||||
|
<FileMenu
|
||||||
|
prevFileContent={prevFileContent}
|
||||||
|
currentFileContent={fileContent}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
setOpenFilePathInputModal={setOpenFilePathInputModal}
|
||||||
|
saveFile={saveFile}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
height: 'calc(100vh - 170px)',
|
||||||
|
padding: '10px',
|
||||||
|
overflow: 'auto',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
elevation={3}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
height="98%"
|
||||||
|
language="sas"
|
||||||
|
value={fileContent}
|
||||||
|
editorDidMount={handleEditorDidMount}
|
||||||
|
options={{ readOnly: ctrlPressed }}
|
||||||
|
onChange={(val) => setFileContent(val)}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: -10,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '13px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Press CTRL + ENTER to run code
|
||||||
|
</p>
|
||||||
|
</Paper>
|
||||||
|
</StyledTabPanel>
|
||||||
|
<StyledTabPanel value="2">
|
||||||
|
<div style={{ marginTop: '50px' }}>
|
||||||
|
<h2>SAS Log</h2>
|
||||||
|
<pre>{log}</pre>
|
||||||
|
</div>
|
||||||
|
</StyledTabPanel>
|
||||||
|
<StyledTabPanel value="3">
|
||||||
|
<div style={{ marginTop: '50px' }}>
|
||||||
|
<pre>{webout}</pre>
|
||||||
|
</div>
|
||||||
|
</StyledTabPanel>
|
||||||
|
</TabContext>
|
||||||
|
)}
|
||||||
|
<Modal
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
title={modalTitle}
|
||||||
|
payload={modalPayload}
|
||||||
|
/>
|
||||||
|
<BootstrapSnackbar
|
||||||
|
open={openSnackbar}
|
||||||
|
setOpen={setOpenSnackbar}
|
||||||
|
message={snackbarMessage}
|
||||||
|
severity={snackbarSeverity}
|
||||||
|
/>
|
||||||
|
<FilePathInputModal
|
||||||
|
open={openFilePathInputModal}
|
||||||
|
setOpen={setOpenFilePathInputModal}
|
||||||
|
saveFile={handleFilePathInput}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SASjsEditor
|
||||||
|
|
||||||
|
type RunMenuProps = {
|
||||||
|
selectedFilePath: string
|
||||||
|
selectedRunTime: string
|
||||||
|
runTimes: string[]
|
||||||
|
handleChangeRunTime: (event: SelectChangeEvent) => void
|
||||||
|
handleRunBtnClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunMenu = ({
|
||||||
|
selectedFilePath,
|
||||||
|
selectedRunTime,
|
||||||
|
runTimes,
|
||||||
|
handleChangeRunTime,
|
||||||
|
handleRunBtnClick
|
||||||
|
}: RunMenuProps) => {
|
||||||
|
const launchProgram = () => {
|
||||||
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="CTRL+ENTER will also run code">
|
||||||
|
<Button
|
||||||
|
onClick={handleRunBtnClick}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '5px 5px',
|
||||||
|
minWidth: 'unset'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
draggable="false"
|
||||||
|
style={{ width: '25px' }}
|
||||||
|
src="/running-sas.png"
|
||||||
|
></img>
|
||||||
|
<span style={{ fontSize: '12px' }}>RUN</span>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{selectedFilePath ? (
|
||||||
|
<Box sx={{ marginLeft: '10px' }}>
|
||||||
|
<Tooltip title="Launch program in new window">
|
||||||
|
<IconButton onClick={launchProgram}>
|
||||||
|
<RocketLaunch />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
||||||
|
<FormControl variant="standard">
|
||||||
|
<Select
|
||||||
|
labelId="run-time-select-label"
|
||||||
|
id="run-time-select"
|
||||||
|
value={selectedRunTime}
|
||||||
|
onChange={handleChangeRunTime}
|
||||||
|
>
|
||||||
|
{runTimes.map((runTime) => (
|
||||||
|
<MenuItem key={runTime} value={runTime}>
|
||||||
|
{runTime}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileMenuProps = {
|
||||||
|
prevFileContent: string
|
||||||
|
currentFileContent: string
|
||||||
|
selectedFilePath: string
|
||||||
|
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
saveFile: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileMenu = ({
|
||||||
|
prevFileContent,
|
||||||
|
currentFileContent,
|
||||||
|
selectedFilePath,
|
||||||
|
setOpenFilePathInputModal,
|
||||||
|
saveFile
|
||||||
|
}: FileMenuProps) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
|
(EventTarget & HTMLButtonElement) | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const handleMenu = (
|
||||||
|
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
if (event) setAnchorEl(event.currentTarget)
|
||||||
|
else setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveAsBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
setOpenFilePathInputModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveBtnClick = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
saveFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Save File Menu">
|
||||||
|
<IconButton onClick={handleMenu}>
|
||||||
|
<MoreVert />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu
|
||||||
|
id="save-file-menu"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
open={!!anchorEl}
|
||||||
|
onClose={() => handleMenu()}
|
||||||
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<Save />}
|
||||||
|
disabled={
|
||||||
|
!selectedFilePath || prevFileContent === currentFileContent
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveAsBtnClick}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SaveAs />}
|
||||||
|
>
|
||||||
|
Save As
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,253 +1,91 @@
|
|||||||
import React, { useEffect, useRef, useState, useContext } from 'react'
|
import React, { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import {
|
import CssBaseline from '@mui/material/CssBaseline'
|
||||||
Backdrop,
|
import Box from '@mui/material/Box'
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
CircularProgress,
|
|
||||||
FormControl,
|
|
||||||
MenuItem,
|
|
||||||
Paper,
|
|
||||||
Select,
|
|
||||||
SelectChangeEvent,
|
|
||||||
Tab,
|
|
||||||
Tooltip
|
|
||||||
} from '@mui/material'
|
|
||||||
import { makeStyles } from '@mui/styles'
|
|
||||||
import Editor, { EditorDidMount } from 'react-monaco-editor'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import { TabContext, TabList, TabPanel } from '@mui/lab'
|
|
||||||
|
|
||||||
import { AppContext, RunTimeType } from '../../context/appContext'
|
import { TreeNode } from '../../utils/types'
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
import SideBar from './sideBar'
|
||||||
root: {
|
import SASjsEditor from './editor'
|
||||||
fontSize: '1rem',
|
|
||||||
color: 'gray',
|
|
||||||
'&.Mui-selected': {
|
|
||||||
color: 'black'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
subMenu: {
|
|
||||||
marginTop: '25px',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center'
|
|
||||||
},
|
|
||||||
runButton: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '5px 5px',
|
|
||||||
minWidth: 'unset'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
const Studio = () => {
|
const Studio = () => {
|
||||||
const appContext = useContext(AppContext)
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const location = useLocation()
|
const [selectedFilePath, setSelectedFilePath] = useState('')
|
||||||
const [fileContent, setFileContent] = useState('')
|
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||||
const [log, setLog] = useState('')
|
|
||||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
|
||||||
const [webout, setWebout] = useState('')
|
|
||||||
const [tab, setTab] = useState('1')
|
|
||||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
|
||||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
|
||||||
const [isRunning, setIsRunning] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
setSelectedFilePath(searchParams.get('filePath') ?? '')
|
||||||
}, [appContext.runTimes])
|
}, [searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
const fetchDirectoryData = useCallback(() => {
|
||||||
if (runTimes.length) setSelectedRunTime(runTimes[0])
|
|
||||||
}, [runTimes])
|
|
||||||
|
|
||||||
const handleTabChange = (_e: any, newValue: string) => {
|
|
||||||
setTab(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const editorRef = useRef(null as any)
|
|
||||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
|
||||||
editor.focus()
|
|
||||||
editorRef.current = editor
|
|
||||||
}
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
setIsRunning(true)
|
|
||||||
axios
|
axios
|
||||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
.get(`/SASjsApi/drive/fileTree`)
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
const parsedLog = res?.data?.log
|
if (res.data && res.data?.status === 'success') {
|
||||||
.map((logLine: any) => logLine.line)
|
setDirectoryData(res.data.tree)
|
||||||
.join('\n')
|
}
|
||||||
|
})
|
||||||
setLog(parsedLog)
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
setWebout(`${res.data?._webout}`)
|
|
||||||
setTab('2')
|
|
||||||
|
|
||||||
// Scroll to bottom of log
|
|
||||||
window.scrollTo(0, document.body.scrollHeight)
|
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err))
|
|
||||||
.finally(() => setIsRunning(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)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const content = localStorage.getItem('fileContent') ?? ''
|
|
||||||
setFileContent(content)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fileContent.length) {
|
fetchDirectoryData()
|
||||||
localStorage.setItem('fileContent', fileContent)
|
}, [fetchDirectoryData])
|
||||||
|
|
||||||
|
const handleSelect = (filePath: string, refreshSideBar?: boolean) => {
|
||||||
|
setSearchParams({ filePath })
|
||||||
|
if (refreshSideBar) fetchDirectoryData()
|
||||||
}
|
}
|
||||||
}, [fileContent])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const removeFileFromTree = (path: string) => {
|
||||||
const params = new URLSearchParams(location.search)
|
if (directoryData) {
|
||||||
const programPath = params.get('_program')
|
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
|
||||||
|
findAndRemoveNode(newTree, newTree, path)
|
||||||
|
setDirectoryData(newTree)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (programPath?.length)
|
const findAndRemoveNode = (
|
||||||
axios
|
node: TreeNode,
|
||||||
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
|
parentNode: TreeNode,
|
||||||
.then((res: any) => setFileContent(res.data.fileContent))
|
path: string
|
||||||
.catch((err) => console.log(err))
|
) => {
|
||||||
}, [location.search])
|
if (node.relativePath === path) {
|
||||||
|
removeNodeFromParent(parentNode, path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (Array.isArray(node.children)) {
|
||||||
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
|
if (findAndRemoveNode(node.children[i], node, path)) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const classes = useStyles()
|
const removeNodeFromParent = (parent: TreeNode, path: string) => {
|
||||||
|
const index = parent.children.findIndex(
|
||||||
|
(node) => node.relativePath === path
|
||||||
|
)
|
||||||
|
if (index !== -1) {
|
||||||
|
parent.children.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ display: 'flex' }}>
|
||||||
onKeyUp={handleKeyUp}
|
<CssBaseline />
|
||||||
onKeyDown={handleKeyDown}
|
<SideBar
|
||||||
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
selectedFilePath={selectedFilePath}
|
||||||
>
|
directoryData={directoryData}
|
||||||
<TabContext value={tab}>
|
handleSelect={handleSelect}
|
||||||
<Box
|
/>
|
||||||
sx={{
|
<SASjsEditor
|
||||||
borderBottom: 1,
|
selectedFilePath={selectedFilePath}
|
||||||
borderColor: 'divider'
|
setSelectedFilePath={handleSelect}
|
||||||
}}
|
|
||||||
style={{ position: 'fixed', background: 'white', width: '100%' }}
|
|
||||||
>
|
|
||||||
<TabList onChange={handleTabChange} centered>
|
|
||||||
<Tab className={classes.root} label="Code" value="1" />
|
|
||||||
<Tab className={classes.root} label="Log" value="2" />
|
|
||||||
<Tooltip title="Displays content from the _webout fileref">
|
|
||||||
<Tab className={classes.root} label="Webout" value="3" />
|
|
||||||
</Tooltip>
|
|
||||||
</TabList>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<TabPanel sx={{ paddingBottom: 0 }} value="1">
|
|
||||||
<Backdrop
|
|
||||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
|
||||||
open={isRunning}
|
|
||||||
>
|
|
||||||
<CircularProgress color="inherit" />
|
|
||||||
</Backdrop>
|
|
||||||
<div className={classes.subMenu}>
|
|
||||||
<Tooltip title="CTRL+ENTER will also run SAS code">
|
|
||||||
<Button onClick={handleRunBtnClick} className={classes.runButton}>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
draggable="false"
|
|
||||||
style={{ width: '25px' }}
|
|
||||||
src="/running-sas.png"
|
|
||||||
></img>
|
|
||||||
<span style={{ fontSize: '12px' }}>RUN</span>
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
|
|
||||||
<FormControl variant="standard">
|
|
||||||
<Select
|
|
||||||
labelId="run-time-select-label"
|
|
||||||
id="run-time-select"
|
|
||||||
value={selectedRunTime}
|
|
||||||
onChange={handleChangeRunTime}
|
|
||||||
>
|
|
||||||
{runTimes.map((runTime) => (
|
|
||||||
<MenuItem key={runTime} value={runTime}>
|
|
||||||
{runTime}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
height: 'calc(100vh - 170px)',
|
|
||||||
padding: '10px',
|
|
||||||
overflow: 'auto',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
elevation={3}
|
|
||||||
>
|
|
||||||
<Editor
|
|
||||||
height="98%"
|
|
||||||
language="sas"
|
|
||||||
value={fileContent}
|
|
||||||
editorDidMount={handleEditorDidMount}
|
|
||||||
options={{ readOnly: ctrlPressed }}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) setFileContent(val)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: -10,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: '13px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Press CTRL + ENTER to run SAS code
|
|
||||||
</p>
|
|
||||||
</Paper>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value="2">
|
|
||||||
<div style={{ marginTop: '50px' }}>
|
|
||||||
<h2>SAS Log</h2>
|
|
||||||
<pre>{log}</pre>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value="3">
|
|
||||||
<div style={{ marginTop: '50px' }}>
|
|
||||||
<pre>{webout}</pre>
|
|
||||||
</div>
|
|
||||||
</TabPanel>
|
|
||||||
</TabContext>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
54
web/src/containers/Studio/sideBar.tsx
Normal file
54
web/src/containers/Studio/sideBar.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
|
import { Box, Drawer, Toolbar } from '@mui/material'
|
||||||
|
|
||||||
|
import TreeView from '../../components/tree'
|
||||||
|
import { TreeNode } from '../../utils/types'
|
||||||
|
|
||||||
|
const drawerWidth = 240
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedFilePath: string
|
||||||
|
directoryData: TreeNode | null
|
||||||
|
handleSelect: (filePath: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SideBar = ({ selectedFilePath, directoryData, handleSelect }: Props) => {
|
||||||
|
const defaultExpanded = useMemo(() => {
|
||||||
|
const splittedPath = selectedFilePath.split('/')
|
||||||
|
const arr = ['']
|
||||||
|
let nodeId = ''
|
||||||
|
splittedPath.forEach((path) => {
|
||||||
|
if (path !== '') {
|
||||||
|
nodeId += '/' + path
|
||||||
|
arr.push(nodeId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return arr
|
||||||
|
}, [selectedFilePath])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar />
|
||||||
|
<Box sx={{ overflow: 'auto' }}>
|
||||||
|
{directoryData && (
|
||||||
|
<TreeView
|
||||||
|
node={directoryData}
|
||||||
|
selectedFilePath={selectedFilePath}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
defaultExpanded={defaultExpanded}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SideBar
|
||||||
Reference in New Issue
Block a user