mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 03:34:35 +00:00
feat(web): added profile + edit + autoexec changes
This commit is contained in:
@@ -357,7 +357,7 @@ components:
|
|||||||
autoExec:
|
autoExec:
|
||||||
type: string
|
type: string
|
||||||
description: 'User-specific auto-exec code'
|
description: 'User-specific auto-exec code'
|
||||||
example: '<SAS code>'
|
example: ""
|
||||||
required:
|
required:
|
||||||
- displayName
|
- displayName
|
||||||
- username
|
- username
|
||||||
@@ -543,7 +543,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
properties:
|
properties:
|
||||||
user: {properties: {displayName: {type: string}, username: {type: string}}, required: [displayName, username], type: object}
|
user: {properties: {displayName: {type: string}, username: {type: string}, id: {type: number, format: double}}, required: [displayName, username, id], type: object}
|
||||||
loggedIn: {type: boolean}
|
loggedIn: {type: boolean}
|
||||||
required:
|
required:
|
||||||
- user
|
- user
|
||||||
|
|||||||
@@ -119,9 +119,9 @@ filename _webout "${weboutPath}" mod;
|
|||||||
/* dynamic user-provided vars */
|
/* dynamic user-provided vars */
|
||||||
${preProgramVarStatments}
|
${preProgramVarStatments}
|
||||||
|
|
||||||
/* user auto exec starts */
|
/* user autoexec starts */
|
||||||
${otherArgs?.userAutoExec}
|
${otherArgs?.userAutoExec ?? ''}
|
||||||
/* user auto exec ends */
|
/* user autoexec ends */
|
||||||
|
|
||||||
/* actual job code */
|
/* actual job code */
|
||||||
${program}`
|
${program}`
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const getUser = async (
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
isActive: user.isActive,
|
isActive: user.isActive,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
autoExec: getAutoExec ? user.autoExec : undefined
|
autoExec: getAutoExec ? user.autoExec ?? '' : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ const login = async (
|
|||||||
return {
|
return {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
user: {
|
user: {
|
||||||
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
displayName: user.displayName
|
displayName: user.displayName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export interface UserPayload {
|
|||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
/**
|
/**
|
||||||
* User-specific auto-exec code
|
* User-specific auto-exec code
|
||||||
* @example "<SAS code>"
|
* @example ""
|
||||||
*/
|
*/
|
||||||
autoExec?: string
|
autoExec?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
|
|||||||
password: passwordSchema.required(),
|
password: passwordSchema.required(),
|
||||||
isAdmin: Joi.boolean(),
|
isAdmin: Joi.boolean(),
|
||||||
isActive: Joi.boolean(),
|
isActive: Joi.boolean(),
|
||||||
autoExec: Joi.string()
|
autoExec: Joi.string().allow('')
|
||||||
}).validate(data)
|
}).validate(data)
|
||||||
|
|
||||||
export const deleteUserValidation = (
|
export const deleteUserValidation = (
|
||||||
@@ -59,7 +59,7 @@ export const updateUserValidation = (
|
|||||||
displayName: Joi.string().min(6),
|
displayName: Joi.string().min(6),
|
||||||
username: usernameSchema,
|
username: usernameSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
autoExec: Joi.string()
|
autoExec: Joi.string().allow('')
|
||||||
}
|
}
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
validationChecks.isAdmin = Joi.boolean()
|
validationChecks.isAdmin = Joi.boolean()
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import Header from './components/header'
|
|||||||
import Home from './components/home'
|
import Home from './components/home'
|
||||||
import Drive from './containers/Drive'
|
import Drive from './containers/Drive'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
|
import Settings from './containers/Settings'
|
||||||
|
|
||||||
import { AppContext } from './context/appContext'
|
import { AppContext } from './context/appContext'
|
||||||
import AuthCode from './containers/AuthCode'
|
import AuthCode from './containers/AuthCode'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
@@ -44,10 +46,14 @@ function App() {
|
|||||||
<Route exact path="/SASjsStudio">
|
<Route exact path="/SASjsStudio">
|
||||||
<Studio />
|
<Studio />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route exact path="/SASjsSettings">
|
||||||
|
<Settings />
|
||||||
|
</Route>
|
||||||
<Route exact path="/SASjsLogon">
|
<Route exact path="/SASjsLogon">
|
||||||
<AuthCode />
|
<AuthCode />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
<ToastContainer />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useContext } from 'react'
|
import React, { useState, useEffect, useContext } from 'react'
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
MenuItem
|
MenuItem
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
|
import SettingsIcon from '@mui/icons-material/Settings'
|
||||||
|
|
||||||
import Username from './username'
|
import Username from './username'
|
||||||
import { AppContext } from '../context/appContext'
|
import { AppContext } from '../context/appContext'
|
||||||
@@ -20,17 +21,23 @@ const PORT_API = process.env.PORT_API
|
|||||||
const baseUrl =
|
const baseUrl =
|
||||||
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||||
|
|
||||||
|
const validTabs = ['/', '/SASjsDrive', '/SASjsStudio']
|
||||||
|
|
||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const appContext = useContext(AppContext)
|
const appContext = useContext(AppContext)
|
||||||
const [tabValue, setTabValue] = useState(
|
const [tabValue, setTabValue] = useState(
|
||||||
pathname === '/SASjsLogon' ? '/' : pathname
|
validTabs.includes(pathname) ? pathname : '/'
|
||||||
)
|
)
|
||||||
const [anchorEl, setAnchorEl] = useState<
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
(EventTarget & HTMLButtonElement) | null
|
(EventTarget & HTMLButtonElement) | null
|
||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTabValue(validTabs.includes(pathname) ? pathname : '/')
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
const handleMenu = (
|
const handleMenu = (
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
) => {
|
) => {
|
||||||
@@ -46,7 +53,10 @@ const Header = (props: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (appContext.logout) appContext.logout()
|
if (appContext.logout) {
|
||||||
|
handleClose()
|
||||||
|
appContext.logout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -134,6 +144,18 @@ const Header = (props: any) => {
|
|||||||
open={!!anchorEl}
|
open={!!anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
|
<MenuItem sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/SASjsSettings"
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<SettingsIcon />}
|
||||||
|
>
|
||||||
|
Setting
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
<Button variant="contained" color="primary">
|
<Button variant="contained" color="primary">
|
||||||
Logout
|
Logout
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ const Login = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
appContext.setLoggedIn?.(loggedIn)
|
appContext.setUserId?.(user.id)
|
||||||
appContext.setUsername?.(user.username)
|
appContext.setUsername?.(user.username)
|
||||||
appContext.setDisplayName?.(user.displayName)
|
appContext.setDisplayName?.(user.displayName)
|
||||||
|
appContext.setLoggedIn?.(loggedIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { ToastContainer, toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import 'react-toastify/dist/ReactToastify.css'
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
@@ -71,8 +71,6 @@ const AuthCode = () => {
|
|||||||
>
|
>
|
||||||
<Button variant="contained">Copy to Clipboard</Button>
|
<Button variant="contained">Copy to Clipboard</Button>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
|
|
||||||
<ToastContainer />
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
55
web/src/containers/Settings/index.tsx
Normal file
55
web/src/containers/Settings/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { Box, Paper, Tab, styled } from '@mui/material'
|
||||||
|
import TabContext from '@mui/lab/TabContext'
|
||||||
|
import TabList from '@mui/lab/TabList'
|
||||||
|
import TabPanel from '@mui/lab/TabPanel'
|
||||||
|
|
||||||
|
import Profile from './profile'
|
||||||
|
|
||||||
|
const StyledTab = styled(Tab)({
|
||||||
|
background: 'black',
|
||||||
|
margin: '0 5px 5px 0'
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledTabpanel = styled(TabPanel)({
|
||||||
|
flexGrow: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
const [value, setValue] = React.useState('profile')
|
||||||
|
|
||||||
|
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
|
setValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: '65px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabContext value={value}>
|
||||||
|
<Box component={Paper} sx={{ margin: '0 5px', height: '92vh' }}>
|
||||||
|
<TabList
|
||||||
|
TabIndicatorProps={{
|
||||||
|
style: {
|
||||||
|
display: 'none'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
orientation="vertical"
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
<StyledTab label="Profile" value="profile" />
|
||||||
|
</TabList>
|
||||||
|
</Box>
|
||||||
|
<StyledTabpanel value="profile">
|
||||||
|
<Profile />
|
||||||
|
</StyledTabpanel>
|
||||||
|
</TabContext>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Settings
|
||||||
148
web/src/containers/Settings/profile.tsx
Normal file
148
web/src/containers/Settings/profile.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
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 { AppContext } from '../../context/appContext'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
</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 item xs={12}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="autoExec"
|
||||||
|
name="autoExec"
|
||||||
|
onChange={handleChange}
|
||||||
|
multiline
|
||||||
|
rows="4"
|
||||||
|
value={user.autoExec}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
<Divider />
|
||||||
|
<CardActions>
|
||||||
|
<Button type="submit" variant="contained" onClick={handleSubmit}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile
|
||||||
@@ -13,6 +13,8 @@ interface AppContextProps {
|
|||||||
checkingSession: boolean
|
checkingSession: boolean
|
||||||
loggedIn: boolean
|
loggedIn: boolean
|
||||||
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
setLoggedIn: Dispatch<SetStateAction<boolean>> | null
|
||||||
|
userId: number
|
||||||
|
setUserId: Dispatch<SetStateAction<number>> | null
|
||||||
username: string
|
username: string
|
||||||
setUsername: Dispatch<SetStateAction<string>> | null
|
setUsername: Dispatch<SetStateAction<string>> | null
|
||||||
displayName: string
|
displayName: string
|
||||||
@@ -24,6 +26,8 @@ export const AppContext = createContext<AppContextProps>({
|
|||||||
checkingSession: false,
|
checkingSession: false,
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
setLoggedIn: null,
|
setLoggedIn: null,
|
||||||
|
userId: 0,
|
||||||
|
setUserId: null,
|
||||||
username: '',
|
username: '',
|
||||||
setUsername: null,
|
setUsername: null,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
@@ -35,6 +39,7 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
const { children } = props
|
const { children } = props
|
||||||
const [checkingSession, setCheckingSession] = useState(false)
|
const [checkingSession, setCheckingSession] = useState(false)
|
||||||
const [loggedIn, setLoggedIn] = useState(false)
|
const [loggedIn, setLoggedIn] = useState(false)
|
||||||
|
const [userId, setUserId] = useState(0)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState('')
|
||||||
|
|
||||||
@@ -46,9 +51,10 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
.then((res) => res.data)
|
.then((res) => res.data)
|
||||||
.then((data: any) => {
|
.then((data: any) => {
|
||||||
setCheckingSession(false)
|
setCheckingSession(false)
|
||||||
setLoggedIn(true)
|
setUserId(data.id)
|
||||||
setUsername(data.username)
|
setUsername(data.username)
|
||||||
setDisplayName(data.displayName)
|
setDisplayName(data.displayName)
|
||||||
|
setLoggedIn(true)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoggedIn(false)
|
setLoggedIn(false)
|
||||||
@@ -70,6 +76,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
|
|||||||
checkingSession,
|
checkingSession,
|
||||||
loggedIn,
|
loggedIn,
|
||||||
setLoggedIn,
|
setLoggedIn,
|
||||||
|
userId,
|
||||||
|
setUserId,
|
||||||
username,
|
username,
|
||||||
setUsername,
|
setUsername,
|
||||||
displayName,
|
displayName,
|
||||||
|
|||||||
Reference in New Issue
Block a user