import React, { Dispatch, SetStateAction, useEffect, useRef, useState, useContext } from 'react' import axios from 'axios' import { Backdrop, Box, Button, CircularProgress, FormControl, IconButton, Menu, MenuItem, Paper, Select, SelectChangeEvent, Tab, Tooltip, Typography } from '@mui/material' import { styled } from '@mui/material/styles' import { RocketLaunch, MoreVert, Save, SaveAs, Difference, Edit } from '@mui/icons-material' import Editor, { MonacoDiffEditor, DiffEditorDidMount, EditorDidMount, monaco } 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' import { usePrompt, useStateWithCallback } from '../../utils/hooks' 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 tab: string setTab: Dispatch> } const baseUrl = window.location.origin const SASJS_LOGS_SEPARATOR = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784' const SASjsEditor = ({ selectedFilePath, setSelectedFilePath, tab, setTab }: 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.Success ) 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([]) 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) }) } usePrompt( 'Changes you made may not be saved.', prevFileContent !== fileContent && !!selectedFilePath ) 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]) 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) => { 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() || 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) if (filePath) { filePath = filePath.startsWith('/') ? filePath : `/${filePath}` } 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 && 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) }) } return ( theme.zIndex.drawer + 1 }} open={isLoading} > {selectedFilePath && !runTimes.includes(selectedFileExtension) ? ( {showDiff ? ( setFileContent(val)} /> ) : ( setFileContent(val)} /> )} ) : ( Webout } value="webout" /> {showDiff ? ( setFileContent(val)} /> ) : ( setFileContent(val)} /> )}

Press CTRL + ENTER to run code

Log

                {log}
              
{webout}
)}
) } export default SASjsEditor 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 = () => { window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`) } return ( <> {selectedFilePath ? ( ) : ( )} ) } type FileMenuProps = { showDiff: boolean setShowDiff: React.Dispatch> prevFileContent: string currentFileContent: string selectedFilePath: string setOpenFilePathInputModal: React.Dispatch> 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 ) => { 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 ( <> handleMenu()} > ) } const getLanguage = (extension: string) => { if (extension === 'js') return 'javascript' if (extension === 'ts') return 'typescript' if (extension === 'md' || extension === 'mdx') return 'markdown' return extension }