1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-15 21:14:35 +00:00

Merge branch 'main' into issue-139

This commit is contained in:
2022-06-23 17:21:52 +05:00
108 changed files with 15072 additions and 2645 deletions

View File

@@ -1 +1 @@
v16.14.0
v16.15.1

463
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,6 @@
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@monaco-editor/react": "^4.3.1",
"@mui/icons-material": "^5.0.3",
"@mui/lab": "^5.0.0-alpha.50",
"@mui/material": "^5.0.3",
@@ -21,9 +20,14 @@
"@types/node": "^12.20.28",
"@types/react": "^17.0.27",
"axios": "^0.24.0",
"monaco-editor": "^0.33.0",
"monaco-editor-webpack-plugin": "^7.0.1",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0"
"react-monaco-editor": "^0.48.0",
"react-router-dom": "^5.3.0",
"react-toastify": "^9.0.1"
},
"devDependencies": {
"@babel/core": "^7.16.0",
@@ -35,6 +39,7 @@
"@types/dotenv-webpack": "^7.0.3",
"@types/prismjs": "^1.16.6",
"@types/react": "^17.0.37",
"@types/react-copy-to-clipboard": "^5.0.2",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.1",
"babel-loader": "^8.2.3",

View File

@@ -11,6 +11,8 @@ import Studio from './containers/Studio'
import Settings from './containers/Settings'
import { AppContext } from './context/appContext'
import AuthCode from './containers/AuthCode'
import { ToastContainer } from 'react-toastify'
function App() {
const appContext = useContext(AppContext)
@@ -21,9 +23,6 @@ function App() {
<HashRouter>
<Header />
<Switch>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
</Route>
<Route path="/">
<Login />
</Route>
@@ -51,9 +50,10 @@ function App() {
<Settings />
</Route>
<Route exact path="/SASjsLogon">
<Login getCodeOnly />
<AuthCode />
</Route>
</Switch>
<ToastContainer />
</HashRouter>
</ThemeProvider>
)

View File

@@ -21,17 +21,21 @@ const PORT_API = process.env.PORT_API
const baseUrl =
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
const Header = (props: any) => {
const history = useHistory()
const { pathname } = useLocation()
const appContext = useContext(AppContext)
const [tabValue, setTabValue] = useState(pathname)
const [tabValue, setTabValue] = useState(
validTabs.includes(pathname) ? pathname : '/'
)
const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null
>(null)
useEffect(() => {
setTabValue(pathname)
setTabValue(validTabs.includes(pathname) ? pathname : '/')
}, [pathname])
const handleMenu = (
@@ -49,7 +53,10 @@ const Header = (props: any) => {
}
const handleLogout = () => {
if (appContext.logout) appContext.logout()
if (appContext.logout) {
handleClose()
appContext.logout()
}
}
return (
<AppBar
@@ -141,11 +148,12 @@ const Header = (props: any) => {
<Button
component={Link}
to="/SASjsSettings"
onClick={handleClose}
variant="contained"
color="primary"
startIcon={<SettingsIcon />}
>
Setting
Settings
</Button>
</MenuItem>
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>

View File

@@ -9,8 +9,8 @@ const Home = () => {
<CssBaseline />
<h2>Welcome to SASjs Server!</h2>
<p>
This portal provides an interface for executing Stored Programs (drive)
and ad hoc code (studio) against a SAS executable. The source code is
SASjs Server provides a REST interface for executing Stored Programs and
ad hoc code (studio) against SAS and JS executables. The source is
available on{' '}
<a
href="https://github.com/sasjs/server"

View File

@@ -1,88 +1,39 @@
import axios from 'axios'
import React, { useState, useContext } from 'react'
import { useLocation } from 'react-router-dom'
import PropTypes from 'prop-types'
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
import { CssBaseline, Box, TextField, Button } from '@mui/material'
import { AppContext } from '../context/appContext'
const getAuthCode = async (credentials: any) =>
axios.post('/SASjsApi/auth/authorize', credentials).then((res) => res.data)
const login = async (payload: { username: string; password: string }) =>
axios.get('/form').then((res1) =>
axios
.post('/login', payload, {
headers: { 'csrf-token': res1.data.csrfToken }
})
.then((res2) => res2.data)
)
axios.post('/SASLogon/login', payload).then((res) => res.data)
const Login = ({ getCodeOnly }: any) => {
const location = useLocation()
const Login = () => {
const appContext = useContext(AppContext)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('')
let error: boolean
const [displayCode, setDisplayCode] = useState(null)
const handleSubmit = async (e: any) => {
error = false
setErrorMessage('')
e.preventDefault()
if (getCodeOnly) {
const params = new URLSearchParams(location.search)
const responseType = params.get('response_type')
if (responseType === 'code') {
const clientId = params.get('client_id')
const { code } = await getAuthCode({
clientId,
username,
password
}).catch((err: any) => {
error = true
setErrorMessage(err.response.data)
return {}
})
if (!error) return setDisplayCode(code)
return
}
}
const { loggedIn, user } = await login({
username,
password
}).catch((err: any) => {
error = true
setErrorMessage(err.response.data)
return {}
})
if (loggedIn) {
appContext.setLoggedIn?.(loggedIn)
appContext.setUserId?.(user.id)
appContext.setUsername?.(user.username)
appContext.setDisplayName?.(user.displayName)
appContext.setLoggedIn?.(loggedIn)
}
}
if (displayCode) {
return (
<Box className="main">
<CssBaseline />
<br />
<h2>Authorization Code</h2>
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
{displayCode}
</Typography>
<br />
</Box>
)
}
return (
<Box
className="main"
@@ -95,13 +46,6 @@ const Login = ({ getCodeOnly }: any) => {
<CssBaseline />
<br />
<h2 style={{ width: 'auto' }}>Welcome to SASjs Server!</h2>
{getCodeOnly && (
<p style={{ width: 'auto' }}>
Provide credentials to get authorization code.
</p>
)}
<br />
<TextField
id="username"
label="Username"

View File

@@ -0,0 +1,78 @@
import axios from 'axios'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import React, { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { useLocation } from 'react-router-dom'
import { CssBaseline, Box, Typography, Button } from '@mui/material'
const getAuthCode = async (credentials: any) =>
axios.post('/SASLogon/authorize', credentials).then((res) => res.data)
const AuthCode = () => {
const location = useLocation()
const [displayCode, setDisplayCode] = useState('')
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
requestAuthCode()
}, [])
const requestAuthCode = async () => {
setErrorMessage('')
const params = new URLSearchParams(location.search)
const responseType = params.get('response_type')
if (responseType !== 'code')
return setErrorMessage('response type is not support')
const clientId = params.get('client_id')
if (!clientId) return setErrorMessage('clientId is not provided')
setErrorMessage('Fetching auth code... ')
const { code } = await getAuthCode({
clientId
})
.then((res) => {
setErrorMessage('')
return res
})
.catch((err: any) => {
setErrorMessage(err.response.data)
return { code: null }
})
return setDisplayCode(code)
}
return (
<Box className="main">
<CssBaseline />
<br />
<h2>Authorization Code</h2>
{displayCode && (
<Typography m={2} p={3} style={{ overflowWrap: 'anywhere' }}>
{displayCode}
</Typography>
)}
{errorMessage && <Typography>{errorMessage}</Typography>}
<br />
<CopyToClipboard
text={displayCode}
onCopy={() =>
toast.info('Code copied to ClipBoard', {
theme: 'dark',
position: toast.POSITION.BOTTOM_RIGHT
})
}
>
<Button variant="contained">Copy to Clipboard</Button>
</CopyToClipboard>
</Box>
)
}
export default AuthCode

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import Editor from '@monaco-editor/react'
import Editor from 'react-monaco-editor'
import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper'
@@ -94,10 +94,7 @@ const Main = (props: Props) => {
setEditMode(false)
} else {
window.open(
`${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath.replace(
/.sas$/,
''
)}`
`${baseUrl}/SASjsApi/stp/execute?_program=${props.selectedFilePath}`
)
}
}
@@ -125,6 +122,7 @@ const Main = (props: Props) => {
{!isLoading && props?.selectedFilePath && editMode && (
<Editor
height="95%"
language="sas"
value={fileContent}
onChange={(val) => {
if (val) setFileContent(val)

View File

@@ -6,6 +6,7 @@ import TabList from '@mui/lab/TabList'
import TabPanel from '@mui/lab/TabPanel'
import Permission from './permission'
import Profile from './profile'
const StyledTab = styled(Tab)({
background: 'black',
@@ -45,7 +46,9 @@ const Settings = () => {
<StyledTab label="Permission" value="permission" />
</TabList>
</Box>
<StyledTabpanel value="profile">Profile Page</StyledTabpanel>
<StyledTabpanel value="profile">
<Profile />
</StyledTabpanel>
<StyledTabpanel value="permission">
<Permission />
</StyledTabpanel>

View File

@@ -0,0 +1,150 @@
import React, { useState, useEffect, useContext } from 'react'
import axios from 'axios'
import {
Grid,
CircularProgress,
Card,
CardHeader,
Divider,
CardContent,
TextField,
CardActions,
Button,
FormGroup,
FormControlLabel,
Checkbox
} from '@mui/material'
import { toast } from 'react-toastify'
import { AppContext, ModeType } from '../../context/appContext'
const Profile = () => {
const [isLoading, setIsLoading] = useState(false)
const appContext = useContext(AppContext)
const [user, setUser] = useState({} as any)
useEffect(() => {
setIsLoading(true)
axios
.get(`/SASjsApi/user/${appContext.userId}`)
.then((res: any) => {
setUser(res.data)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
setIsLoading(false)
})
}, [])
const handleChange = (event: any) => {
const { name, value } = event.target
setUser({ ...user, [name]: value })
}
const handleSubmit = () => {
setIsLoading(true)
axios
.patch(`/SASjsApi/user/${appContext.userId}`, {
username: user.username,
displayName: user.displayName,
autoExec: user.autoExec
})
.then((res: any) => {
toast.success('User information updated', {
theme: 'dark',
position: toast.POSITION.BOTTOM_RIGHT
})
})
.catch((err) => {
toast.error('Failed: ' + err.response?.data || err.text, {
theme: 'dark',
position: toast.POSITION.BOTTOM_RIGHT
})
})
.finally(() => {
setIsLoading(false)
})
}
return isLoading ? (
<CircularProgress
style={{ position: 'absolute', left: '50%', top: '50%' }}
/>
) : (
<Card>
<CardHeader title="Profile Information" />
<Divider />
<CardContent>
<Grid container spacing={4}>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.displayName?.length === 0}
helperText="Please specify display name"
label="Display Name"
name="displayName"
onChange={handleChange}
required
value={user.displayName}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
error={user.username?.length === 0}
helperText="Please specify username"
label="Username"
name="username"
onChange={handleChange}
required
value={user.username}
variant="outlined"
disabled={appContext.mode === ModeType.Desktop}
/>
</Grid>
<Grid item lg={6} md={8} sm={12} xs={12}>
<TextField
fullWidth
label="autoExec"
name="autoExec"
onChange={handleChange}
multiline
rows="10"
value={user.autoExec}
variant="outlined"
/>
</Grid>
<Grid item xs={6}>
<FormGroup row>
<FormControlLabel
disabled
control={<Checkbox checked={user.isActive} />}
label="isActive"
/>
<FormControlLabel
disabled
control={<Checkbox checked={user.isAdmin} />}
label="isAdmin"
/>
</FormGroup>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions>
<Button type="submit" variant="contained" onClick={handleSubmit}>
Save Changes
</Button>
</CardActions>
</Card>
)
}
export default Profile

View File

@@ -1,13 +1,24 @@
import React, { useEffect, useRef, useState } from 'react'
import React, { useEffect, useRef, useState, useContext } from 'react'
import axios from 'axios'
import Box from '@mui/material/Box'
import { Button, Paper, Stack, Tab, Tooltip } from '@mui/material'
import {
Box,
MenuItem,
FormControl,
Select,
SelectChangeEvent,
Button,
Paper,
Tab,
Tooltip
} from '@mui/material'
import { makeStyles } from '@mui/styles'
import Editor, { OnMount } from '@monaco-editor/react'
import Editor, { EditorDidMount } from 'react-monaco-editor'
import { useLocation } from 'react-router-dom'
import { TabContext, TabList, TabPanel } from '@mui/lab'
import { AppContext, RunTimeType } from '../../context/appContext'
const useStyles = makeStyles(() => ({
root: {
fontSize: '1rem',
@@ -30,19 +41,30 @@ const useStyles = makeStyles(() => ({
}))
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] = React.useState('1')
const [tab, setTab] = useState('1')
const [runTimes, setRunTimes] = useState<string[]>([])
const [selectedRunTime, setSelectedRunTime] = useState('')
useEffect(() => {
setRunTimes(Object.values(appContext.runTimes))
}, [appContext.runTimes])
useEffect(() => {
if (runTimes.length) setSelectedRunTime(runTimes[0])
}, [runTimes])
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
const editorRef = useRef(null as any)
const handleEditorDidMount: OnMount = (editor) => {
const handleEditorDidMount: EditorDidMount = (editor) => {
editor.focus()
editorRef.current = editor
}
@@ -57,7 +79,7 @@ const Studio = () => {
const runCode = (code: string) => {
axios
.post(`/SASjsApi/code/execute`, { code })
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
.then((res: any) => {
const parsedLog = res?.data?.log
.map((logLine: any) => logLine.line)
@@ -89,6 +111,10 @@ const Studio = () => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
}
const handleChangeRunTime = (event: SelectChangeEvent) => {
setSelectedRunTime(event.target.value as RunTimeType)
}
useEffect(() => {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
@@ -136,11 +162,12 @@ const Studio = () => {
</TabList>
</Box>
<TabPanel style={{ paddingBottom: 0 }} value="1">
<TabPanel sx={{ paddingBottom: 0 }} value="1">
<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"
@@ -148,8 +175,23 @@ const Studio = () => {
<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>
{/* <Toolbar /> */}
<Paper
sx={{
height: 'calc(100vh - 170px)',
@@ -161,8 +203,9 @@ const Studio = () => {
>
<Editor
height="98%"
language="sas"
value={fileContent}
onMount={handleEditorDidMount}
editorDidMount={handleEditorDidMount}
options={{ readOnly: ctrlPressed }}
onChange={(val) => {
if (val) setFileContent(val)

View File

@@ -9,14 +9,28 @@ import React, {
} from 'react'
import axios from 'axios'
export enum ModeType {
Server = 'server',
Desktop = 'desktop'
}
export enum RunTimeType {
SAS = 'sas',
JS = 'js'
}
interface AppContextProps {
checkingSession: boolean
loggedIn: boolean
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
userId: number
setUserId: Dispatch<SetStateAction<number>> | null
username: string
setUsername: Dispatch<SetStateAction<string>> | null
displayName: string
setDisplayName: Dispatch<SetStateAction<string>> | null
mode: ModeType
runTimes: RunTimeType[]
logout: (() => void) | null
}
@@ -24,10 +38,14 @@ export const AppContext = createContext<AppContextProps>({
checkingSession: false,
loggedIn: false,
setLoggedIn: null,
userId: 0,
setUserId: null,
username: '',
setUsername: null,
displayName: '',
setDisplayName: null,
mode: ModeType.Server,
runTimes: [],
logout: null
})
@@ -35,8 +53,11 @@ const AppContextProvider = (props: { children: ReactNode }) => {
const { children } = props
const [checkingSession, setCheckingSession] = useState(false)
const [loggedIn, setLoggedIn] = useState(false)
const [userId, setUserId] = useState(0)
const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('')
const [mode, setMode] = useState(ModeType.Server)
const [runTimes, setRunTimes] = useState<RunTimeType[]>([])
useEffect(() => {
setCheckingSession(true)
@@ -46,17 +67,28 @@ const AppContextProvider = (props: { children: ReactNode }) => {
.then((res) => res.data)
.then((data: any) => {
setCheckingSession(false)
setLoggedIn(true)
setUserId(data.id)
setUsername(data.username)
setDisplayName(data.displayName)
setLoggedIn(true)
})
.catch(() => {
setLoggedIn(false)
axios.get('/') // get CSRF TOKEN
})
axios
.get('/SASjsApi/info')
.then((res) => res.data)
.then((data: any) => {
setMode(data.mode)
setRunTimes(data.runTimes)
})
.catch(() => {})
}, [])
const logout = useCallback(() => {
axios.get('/logout').then(() => {
axios.get('/SASLogon/logout').then(() => {
setLoggedIn(false)
setUsername('')
setDisplayName('')
@@ -69,10 +101,14 @@ const AppContextProvider = (props: { children: ReactNode }) => {
checkingSession,
loggedIn,
setLoggedIn,
userId,
setUserId,
username,
setUsername,
displayName,
setDisplayName,
mode,
runTimes,
logout
}}
>

View File

@@ -1,4 +1,5 @@
import path from 'path'
import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'
import { Configuration } from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import CopyPlugin from 'copy-webpack-plugin'
@@ -53,7 +54,8 @@ const config: Configuration = {
new CopyPlugin({
patterns: [{ from: 'public' }]
}),
new dotenv()
new dotenv(),
new MonacoWebpackPlugin()
]
}