mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +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 {
|
||||
Backdrop,
|
||||
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 CssBaseline from '@mui/material/CssBaseline'
|
||||
import Box from '@mui/material/Box'
|
||||
|
||||
import { AppContext, RunTimeType } from '../../context/appContext'
|
||||
import { TreeNode } from '../../utils/types'
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
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'
|
||||
}
|
||||
}))
|
||||
import SideBar from './sideBar'
|
||||
import SASjsEditor from './editor'
|
||||
|
||||
const Studio = () => {
|
||||
const appContext = useContext(AppContext)
|
||||
const location = useLocation()
|
||||
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 [isRunning, setIsRunning] = useState(false)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [selectedFilePath, setSelectedFilePath] = useState('')
|
||||
const [directoryData, setDirectoryData] = useState<TreeNode | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setRunTimes(Object.values(appContext.runTimes))
|
||||
}, [appContext.runTimes])
|
||||
setSelectedFilePath(searchParams.get('filePath') ?? '')
|
||||
}, [searchParams])
|
||||
|
||||
useEffect(() => {
|
||||
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)
|
||||
const fetchDirectoryData = useCallback(() => {
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||
.get(`/SASjsApi/drive/fileTree`)
|
||||
.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)
|
||||
if (res.data && res.data?.status === 'success') {
|
||||
setDirectoryData(res.data.tree)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
.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(() => {
|
||||
if (fileContent.length) {
|
||||
localStorage.setItem('fileContent', fileContent)
|
||||
fetchDirectoryData()
|
||||
}, [fetchDirectoryData])
|
||||
|
||||
const handleSelect = (filePath: string, refreshSideBar?: boolean) => {
|
||||
setSearchParams({ filePath })
|
||||
if (refreshSideBar) fetchDirectoryData()
|
||||
}
|
||||
|
||||
const removeFileFromTree = (path: string) => {
|
||||
if (directoryData) {
|
||||
const newTree = JSON.parse(JSON.stringify(directoryData)) as TreeNode
|
||||
findAndRemoveNode(newTree, newTree, path)
|
||||
setDirectoryData(newTree)
|
||||
}
|
||||
}, [fileContent])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search)
|
||||
const programPath = params.get('_program')
|
||||
const findAndRemoveNode = (
|
||||
node: TreeNode,
|
||||
parentNode: TreeNode,
|
||||
path: string
|
||||
) => {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (programPath?.length)
|
||||
axios
|
||||
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
|
||||
.then((res: any) => setFileContent(res.data.fileContent))
|
||||
.catch((err) => console.log(err))
|
||||
}, [location.search])
|
||||
|
||||
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 (
|
||||
<Box
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}
|
||||
>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
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 sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
<SideBar
|
||||
selectedFilePath={selectedFilePath}
|
||||
directoryData={directoryData}
|
||||
handleSelect={handleSelect}
|
||||
/>
|
||||
<SASjsEditor
|
||||
selectedFilePath={selectedFilePath}
|
||||
setSelectedFilePath={handleSelect}
|
||||
/>
|
||||
</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