mirror of
https://github.com/sasjs/server.git
synced 2026-01-08 23:10:05 +00:00
chore(web): refactor react code
This commit is contained in:
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
112
web/src/containers/Studio/internal/components/fileMenu.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Button, IconButton, Menu, MenuItem, Tooltip } from '@mui/material'
|
||||
|
||||
import { MoreVert, Save, SaveAs, Difference, Edit } from '@mui/icons-material'
|
||||
|
||||
type FileMenuProps = {
|
||||
showDiff: boolean
|
||||
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
|
||||
prevFileContent: string
|
||||
currentFileContent: string
|
||||
selectedFilePath: string
|
||||
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
|
||||
saveFile: () => void
|
||||
}
|
||||
|
||||
const FileMenu = ({
|
||||
showDiff,
|
||||
setShowDiff,
|
||||
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 handleDiffBtnClick = () => {
|
||||
setAnchorEl(null)
|
||||
setShowDiff(!showDiff)
|
||||
}
|
||||
|
||||
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={handleDiffBtnClick}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={showDiff ? <Edit /> : <Difference />}
|
||||
>
|
||||
{showDiff ? 'Edit' : 'Diff'}
|
||||
</Button>
|
||||
</MenuItem>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileMenu
|
||||
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
100
web/src/containers/Studio/internal/components/runMenu.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
|
||||
import { RocketLaunch } from '@mui/icons-material'
|
||||
|
||||
type RunMenuProps = {
|
||||
selectedFilePath: string
|
||||
fileContent: string
|
||||
prevFileContent: string
|
||||
selectedRunTime: string
|
||||
runTimes: string[]
|
||||
handleChangeRunTime: (event: SelectChangeEvent) => void
|
||||
handleRunBtnClick: () => void
|
||||
}
|
||||
|
||||
const RunMenu = ({
|
||||
selectedFilePath,
|
||||
fileContent,
|
||||
prevFileContent,
|
||||
selectedRunTime,
|
||||
runTimes,
|
||||
handleChangeRunTime,
|
||||
handleRunBtnClick
|
||||
}: RunMenuProps) => {
|
||||
const launchProgram = () => {
|
||||
const baseUrl = window.location.origin
|
||||
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={
|
||||
fileContent !== prevFileContent
|
||||
? 'Save file before launching program'
|
||||
: 'Launch program in new window'
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={fileContent !== prevFileContent}
|
||||
onClick={launchProgram}
|
||||
>
|
||||
<RocketLaunch />
|
||||
</IconButton>
|
||||
</span>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RunMenu
|
||||
14
web/src/containers/Studio/internal/helper.ts
Normal file
14
web/src/containers/Studio/internal/helper.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const getLanguageFromExtension = (extension: string) => {
|
||||
if (extension === 'js') return 'javascript'
|
||||
|
||||
if (extension === 'ts') return 'typescript'
|
||||
|
||||
if (extension === 'md' || extension === 'mdx') return 'markdown'
|
||||
|
||||
return extension
|
||||
}
|
||||
|
||||
export const getSelection = (editor: any) => {
|
||||
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
|
||||
return selection ?? ''
|
||||
}
|
||||
299
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
299
web/src/containers/Studio/internal/hooks/useEditor.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { DiffEditorDidMount, EditorDidMount, monaco } from 'react-monaco-editor'
|
||||
import { SelectChangeEvent } from '@mui/material'
|
||||
import { getSelection } from '../helper'
|
||||
import { AppContext, RunTimeType } from '../../../../context/appContext'
|
||||
import { AlertSeverityType } from '../../../../components/snackbar'
|
||||
import {
|
||||
useModal,
|
||||
useSnackbar,
|
||||
useStateWithCallback
|
||||
} from '../../../../utils/hooks'
|
||||
|
||||
const SASJS_LOGS_SEPARATOR =
|
||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
|
||||
type UseEditorParams = {
|
||||
selectedFilePath: string
|
||||
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
|
||||
setTab: Dispatch<SetStateAction<string>>
|
||||
}
|
||||
|
||||
const useEditor = ({
|
||||
selectedFilePath,
|
||||
setSelectedFilePath,
|
||||
setTab
|
||||
}: UseEditorParams) => {
|
||||
const appContext = useContext(AppContext)
|
||||
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
|
||||
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||
useSnackbar()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState('')
|
||||
const [ctrlPressed, setCtrlPressed] = useState(false)
|
||||
const [webout, setWebout] = useState('')
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState('')
|
||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||
const [showDiff, setShowDiff] = useState(false)
|
||||
|
||||
const editorRef = useRef(null as any)
|
||||
|
||||
const handleEditorDidMount: EditorDidMount = (editor) => {
|
||||
editorRef.current = editor
|
||||
editor.focus()
|
||||
editor.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'show-difference',
|
||||
|
||||
// A label of the action that will be presented to the user.
|
||||
label: 'Show Differences',
|
||||
|
||||
// An optional array of keybindings for the action.
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
|
||||
|
||||
contextMenuGroupId: 'navigation',
|
||||
|
||||
contextMenuOrder: 1,
|
||||
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: function (ed) {
|
||||
setShowDiff(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
|
||||
diffEditor.focus()
|
||||
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
|
||||
setShowDiff(false)
|
||||
})
|
||||
}
|
||||
|
||||
const saveFile = useCallback(
|
||||
(filePath?: string) => {
|
||||
setIsLoading(true)
|
||||
|
||||
if (filePath) {
|
||||
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||
formData.append('file', stringBlob)
|
||||
formData.append('filePath', filePath ?? selectedFilePath)
|
||||
|
||||
const axiosPromise = filePath
|
||||
? axios.post('/SASjsApi/drive/file', formData)
|
||||
: axios.patch('/SASjsApi/drive/file', formData)
|
||||
|
||||
axiosPromise
|
||||
.then(() => {
|
||||
if (filePath && fileContent === prevFileContent) {
|
||||
// when fileContent and prevFileContent is same,
|
||||
// callback function in setPrevFileContent method is not called
|
||||
// because behind the scene useEffect hook is being used
|
||||
// for calling callback function, and it's only fired when the
|
||||
// new value is not equal to old value.
|
||||
// So, we'll have to explicitly update the selected file path
|
||||
|
||||
setSelectedFilePath(filePath, true)
|
||||
} else {
|
||||
setPrevFileContent(fileContent, () => {
|
||||
if (filePath) {
|
||||
setSelectedFilePath(filePath, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
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)
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fileContent, prevFileContent, selectedFilePath]
|
||||
)
|
||||
|
||||
const handleTabChange = (_e: any, newValue: string) => {
|
||||
setTab(newValue)
|
||||
}
|
||||
|
||||
const handleRunBtnClick = () =>
|
||||
runCode(getSelection(editorRef.current as any) || fileContent)
|
||||
|
||||
const runCode = (code: string) => {
|
||||
setIsLoading(true)
|
||||
axios
|
||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||
.then((res: any) => {
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
setTab('log')
|
||||
|
||||
// Scroll to bottom of log
|
||||
const logElement = document.getElementById('log')
|
||||
if (logElement) logElement.scrollTop = logElement.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(editorRef.current as any) || 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)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current.addAction({
|
||||
// An unique identifier of the contributed action.
|
||||
id: 'save-file',
|
||||
|
||||
// A label of the action that will be presented to the user.
|
||||
label: 'Save',
|
||||
|
||||
// An optional array of keybindings for the action.
|
||||
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: () => {
|
||||
if (!selectedFilePath) return setOpenFilePathInputModal(true)
|
||||
if (prevFileContent !== fileContent) return saveFile()
|
||||
}
|
||||
})
|
||||
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
|
||||
|
||||
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))
|
||||
} else {
|
||||
const content = localStorage.getItem('fileContent') ?? ''
|
||||
setFileContent(content)
|
||||
}
|
||||
setLog('')
|
||||
setWebout('')
|
||||
setTab('code')
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedFilePath])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileContent.length && !selectedFilePath) {
|
||||
localStorage.setItem('fileContent', fileContent)
|
||||
}
|
||||
}, [fileContent, selectedFilePath])
|
||||
|
||||
useEffect(() => {
|
||||
if (runTimes.includes(selectedFileExtension))
|
||||
setSelectedRunTime(selectedFileExtension)
|
||||
}, [selectedFileExtension, runTimes])
|
||||
|
||||
return {
|
||||
ctrlPressed,
|
||||
fileContent,
|
||||
isLoading,
|
||||
log,
|
||||
openFilePathInputModal,
|
||||
prevFileContent,
|
||||
runTimes,
|
||||
selectedFileExtension,
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
handleEditorDidMount,
|
||||
handleFilePathInput,
|
||||
handleKeyDown,
|
||||
handleKeyUp,
|
||||
handleRunBtnClick,
|
||||
handleTabChange,
|
||||
saveFile,
|
||||
setShowDiff,
|
||||
setOpenFilePathInputModal,
|
||||
setFileContent,
|
||||
Snackbar
|
||||
}
|
||||
}
|
||||
|
||||
export default useEditor
|
||||
Reference in New Issue
Block a user