setFileContent(val)}
- />
- )}
+ {monacoEditor}
)}
-
-
+
+
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 (
- <>
-
-
-
-
-
-
- >
- )
-}
-
-const getLanguage = (extension: string) => {
- if (extension === 'js') return 'javascript'
-
- if (extension === 'ts') return 'typescript'
-
- if (extension === 'md' || extension === 'mdx') return 'markdown'
-
- return extension
-}
diff --git a/web/src/containers/Studio/internal/components/fileMenu.tsx b/web/src/containers/Studio/internal/components/fileMenu.tsx
new file mode 100644
index 0000000..d1fe317
--- /dev/null
+++ b/web/src/containers/Studio/internal/components/fileMenu.tsx
@@ -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>
+ 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 (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+export default FileMenu
diff --git a/web/src/containers/Studio/internal/components/runMenu.tsx b/web/src/containers/Studio/internal/components/runMenu.tsx
new file mode 100644
index 0000000..0ab05e9
--- /dev/null
+++ b/web/src/containers/Studio/internal/components/runMenu.tsx
@@ -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 (
+ <>
+
+
+
+ {selectedFilePath ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+ >
+ )
+}
+
+export default RunMenu
diff --git a/web/src/containers/Studio/internal/helper.ts b/web/src/containers/Studio/internal/helper.ts
new file mode 100644
index 0000000..b368912
--- /dev/null
+++ b/web/src/containers/Studio/internal/helper.ts
@@ -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 ?? ''
+}
diff --git a/web/src/containers/Studio/internal/hooks/useEditor.ts b/web/src/containers/Studio/internal/hooks/useEditor.ts
new file mode 100644
index 0000000..7cb8dd3
--- /dev/null
+++ b/web/src/containers/Studio/internal/hooks/useEditor.ts
@@ -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>
+}
+
+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([])
+ 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
diff --git a/web/src/context/permissionsContext.tsx b/web/src/context/permissionsContext.tsx
new file mode 100644
index 0000000..e59f509
--- /dev/null
+++ b/web/src/context/permissionsContext.tsx
@@ -0,0 +1,120 @@
+import React, {
+ createContext,
+ Dispatch,
+ SetStateAction,
+ useState,
+ useCallback,
+ ReactNode
+} from 'react'
+import axios from 'axios'
+import { PermissionResponse } from '../utils/types'
+import { useModal, useSnackbar } from '../utils/hooks'
+import { AlertSeverityType } from '../components/snackbar'
+import usePermissionResponseModal from '../containers/Settings/internal/hooks/usePermissionResponseModal'
+import { PermissionResponsePayload } from '../containers/Settings/internal/components/permissionResponseModal'
+
+interface PermissionsContextProps {
+ isLoading: boolean
+ setIsLoading: Dispatch>
+ permissions: PermissionResponse[]
+ setPermissions: Dispatch>
+ selectedPermission: PermissionResponse | undefined
+ setSelectedPermission: Dispatch<
+ React.SetStateAction
+ >
+ filteredPermissions: PermissionResponse[]
+ setFilteredPermissions: Dispatch>
+ filterApplied: boolean
+ setFilterApplied: Dispatch>
+ fetchPermissions: () => void
+ Dialog: () => JSX.Element
+ setOpenModal: Dispatch>
+ setModalTitle: Dispatch>
+ setModalPayload: Dispatch>
+ Snackbar: () => JSX.Element
+ setOpenSnackbar: Dispatch>
+ setSnackbarMessage: Dispatch>
+ setSnackbarSeverity: Dispatch>
+ PermissionResponseDialog: () => JSX.Element
+ setOpenPermissionResponseModal: Dispatch>
+ setPermissionResponsePayload: Dispatch<
+ React.SetStateAction
+ >
+}
+
+export const PermissionsContext = createContext(
+ undefined!
+)
+
+const PermissionsContextProvider = (props: { children: ReactNode }) => {
+ const { children } = props
+ const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
+ const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
+ useSnackbar()
+ const {
+ PermissionResponseDialog,
+ setOpenPermissionResponseModal,
+ setPermissionResponsePayload
+ } = usePermissionResponseModal()
+ const [isLoading, setIsLoading] = useState(false)
+ const [permissions, setPermissions] = useState([])
+ const [selectedPermission, setSelectedPermission] =
+ useState()
+ const [filteredPermissions, setFilteredPermissions] = useState<
+ PermissionResponse[]
+ >([])
+ const [filterApplied, setFilterApplied] = useState(false)
+
+ const fetchPermissions = useCallback(() => {
+ axios
+ .get(`/SASjsApi/permission`)
+ .then((res: any) => {
+ if (res.data?.length > 0) {
+ setPermissions(res.data)
+ }
+ })
+ .catch((err) => {
+ setModalTitle('Abort')
+ setModalPayload(
+ typeof err.response.data === 'object'
+ ? JSON.stringify(err.response.data)
+ : err.response.data
+ )
+ setOpenModal(true)
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default PermissionsContextProvider
diff --git a/web/src/utils/hooks/index.ts b/web/src/utils/hooks/index.ts
index bb694d1..48f7886 100644
--- a/web/src/utils/hooks/index.ts
+++ b/web/src/utils/hooks/index.ts
@@ -1,2 +1,4 @@
+export * from './useModal'
export * from './usePrompt'
export * from './useStateWithCallback'
+export * from './useSnackbar'
diff --git a/web/src/utils/hooks/useModal.tsx b/web/src/utils/hooks/useModal.tsx
new file mode 100644
index 0000000..8553e2a
--- /dev/null
+++ b/web/src/utils/hooks/useModal.tsx
@@ -0,0 +1,19 @@
+import { useState } from 'react'
+import Modal from '../../components/modal'
+
+export const useModal = () => {
+ const [openModal, setOpenModal] = useState(false)
+ const [modalTitle, setModalTitle] = useState('')
+ const [modalPayload, setModalPayload] = useState('')
+
+ const Dialog = () => (
+
+ )
+
+ return { Dialog, setOpenModal, setModalTitle, setModalPayload }
+}
diff --git a/web/src/utils/hooks/useSnackbar.tsx b/web/src/utils/hooks/useSnackbar.tsx
new file mode 100644
index 0000000..0fbc304
--- /dev/null
+++ b/web/src/utils/hooks/useSnackbar.tsx
@@ -0,0 +1,21 @@
+import { useState } from 'react'
+import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
+
+export const useSnackbar = () => {
+ const [openSnackbar, setOpenSnackbar] = useState(false)
+ const [snackbarMessage, setSnackbarMessage] = useState('')
+ const [snackbarSeverity, setSnackbarSeverity] = useState(
+ AlertSeverityType.Success
+ )
+
+ const Snackbar = () => (
+
+ )
+
+ return { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity }
+}