From 6c35412d2f5180d4e49b12e616576d8b8dacb7d8 Mon Sep 17 00:00:00 2001 From: Sabir Hassan Date: Mon, 18 Jul 2022 22:39:09 +0500 Subject: [PATCH] feat: add sidebar(drive) to left of studio --- web/src/containers/Studio/editor.tsx | 519 ++++++++++++++++++++++++++ web/src/containers/Studio/index.tsx | 298 ++++----------- web/src/containers/Studio/sideBar.tsx | 54 +++ 3 files changed, 641 insertions(+), 230 deletions(-) create mode 100644 web/src/containers/Studio/editor.tsx create mode 100644 web/src/containers/Studio/sideBar.tsx diff --git a/web/src/containers/Studio/editor.tsx b/web/src/containers/Studio/editor.tsx new file mode 100644 index 0000000..4889282 --- /dev/null +++ b/web/src/containers/Studio/editor.tsx @@ -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.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([]) + 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 ( + + theme.zIndex.drawer + 1 }} + open={isLoading} + > + + + {selectedFilePath && !runTimes.includes(selectedFileExtension) ? ( + + + + + + setFileContent(val)} + /> + + + ) : ( + + + + + + + + + + + + + + + + + + setFileContent(val)} + /> +

+ Press CTRL + ENTER to run code +

+
+
+ +
+

SAS Log

+
{log}
+
+
+ +
+
{webout}
+
+
+
+ )} + + + +
+ ) +} + +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 ( + <> + + + + {selectedFilePath ? ( + + + + + + + + ) : ( + + + + + + )} + + ) +} + +type FileMenuProps = { + prevFileContent: string + currentFileContent: string + selectedFilePath: string + setOpenFilePathInputModal: React.Dispatch> + saveFile: () => void +} + +const FileMenu = ({ + 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 handleSaveAsBtnClick = () => { + setAnchorEl(null) + setOpenFilePathInputModal(true) + } + + const handleSaveBtnClick = () => { + setAnchorEl(null) + saveFile() + } + + return ( + <> + + + + + + handleMenu()} + > + + + + + + + + + ) +} diff --git a/web/src/containers/Studio/index.tsx b/web/src/containers/Studio/index.tsx index 011ce05..5d556ca 100644 --- a/web/src/containers/Studio/index.tsx +++ b/web/src/containers/Studio/index.tsx @@ -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([]) - const [selectedRunTime, setSelectedRunTime] = useState('') - const [isRunning, setIsRunning] = useState(false) + const [searchParams, setSearchParams] = useSearchParams() + const [selectedFilePath, setSelectedFilePath] = useState('') + const [directoryData, setDirectoryData] = useState(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 ( - - - - - - - - - - - - - - theme.zIndex.drawer + 1 }} - open={isRunning} - > - - -
- - - - - - - - -
- - { - if (val) setFileContent(val) - }} - /> -

- Press CTRL + ENTER to run SAS code -

-
-
- -
-

SAS Log

-
{log}
-
-
- -
-
{webout}
-
-
-
+ + + + ) } diff --git a/web/src/containers/Studio/sideBar.tsx b/web/src/containers/Studio/sideBar.tsx new file mode 100644 index 0000000..9f18e08 --- /dev/null +++ b/web/src/containers/Studio/sideBar.tsx @@ -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 ( + + + + {directoryData && ( + + )} + + + ) +} + +export default SideBar