1
0
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:
2022-07-18 22:39:09 +05:00
parent 27410bc32b
commit 6c35412d2f
3 changed files with 641 additions and 230 deletions

View 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>
</>
)
}

View File

@@ -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>
)
}

View 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