1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

feat(web): added profile + edit + autoexec changes

This commit is contained in:
Saad Jutt
2022-05-26 04:25:15 +05:00
parent e4239fbcc3
commit c275db184e
13 changed files with 256 additions and 17 deletions

View File

@@ -357,7 +357,7 @@ components:
autoExec:
type: string
description: 'User-specific auto-exec code'
example: '<SAS code>'
example: ""
required:
- displayName
- username
@@ -543,7 +543,7 @@ paths:
application/json:
schema:
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}
required:
- user

View File

@@ -119,9 +119,9 @@ filename _webout "${weboutPath}" mod;
/* dynamic user-provided vars */
${preProgramVarStatments}
/* user auto exec starts */
${otherArgs?.userAutoExec}
/* user auto exec ends */
/* user autoexec starts */
${otherArgs?.userAutoExec ?? ''}
/* user autoexec ends */
/* actual job code */
${program}`

View File

@@ -177,7 +177,7 @@ const getUser = async (
username: user.username,
isActive: user.isActive,
isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec : undefined
autoExec: getAutoExec ? user.autoExec ?? '' : undefined
}
}

View File

@@ -97,6 +97,7 @@ const login = async (
return {
loggedIn: true,
user: {
id: user.id,
username: user.username,
displayName: user.displayName
}

View File

@@ -29,7 +29,7 @@ export interface UserPayload {
isActive?: boolean
/**
* User-specific auto-exec code
* @example "<SAS code>"
* @example ""
*/
autoExec?: string
}

View File

@@ -36,7 +36,7 @@ export const registerUserValidation = (data: any): Joi.ValidationResult =>
password: passwordSchema.required(),
isAdmin: Joi.boolean(),
isActive: Joi.boolean(),
autoExec: Joi.string()
autoExec: Joi.string().allow('')
}).validate(data)
export const deleteUserValidation = (
@@ -59,7 +59,7 @@ export const updateUserValidation = (
displayName: Joi.string().min(6),
username: usernameSchema,
password: passwordSchema,
autoExec: Joi.string()
autoExec: Joi.string().allow('')
}
if (isAdmin) {
validationChecks.isAdmin = Joi.boolean()

View File

@@ -8,9 +8,11 @@ import Header from './components/header'
import Home from './components/home'
import Drive from './containers/Drive'
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)
@@ -44,10 +46,14 @@ function App() {
<Route exact path="/SASjsStudio">
<Studio />
</Route>
<Route exact path="/SASjsSettings">
<Settings />
</Route>
<Route exact path="/SASjsLogon">
<AuthCode />
</Route>
</Switch>
<ToastContainer />
</HashRouter>
</ThemeProvider>
)

View File

@@ -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 {
@@ -11,6 +11,7 @@ import {
MenuItem
} from '@mui/material'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import SettingsIcon from '@mui/icons-material/Settings'
import Username from './username'
import { AppContext } from '../context/appContext'
@@ -20,17 +21,23 @@ 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 === '/SASjsLogon' ? '/' : pathname
validTabs.includes(pathname) ? pathname : '/'
)
const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null
>(null)
useEffect(() => {
setTabValue(validTabs.includes(pathname) ? pathname : '/')
}, [pathname])
const handleMenu = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
@@ -46,7 +53,10 @@ const Header = (props: any) => {
}
const handleLogout = () => {
if (appContext.logout) appContext.logout()
if (appContext.logout) {
handleClose()
appContext.logout()
}
}
return (
<AppBar
@@ -134,6 +144,18 @@ const Header = (props: any) => {
open={!!anchorEl}
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' }}>
<Button variant="contained" color="primary">
Logout

View File

@@ -27,9 +27,10 @@ const Login = () => {
})
if (loggedIn) {
appContext.setLoggedIn?.(loggedIn)
appContext.setUserId?.(user.id)
appContext.setUsername?.(user.username)
appContext.setDisplayName?.(user.displayName)
appContext.setLoggedIn?.(loggedIn)
}
}

View File

@@ -1,7 +1,7 @@
import axios from 'axios'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import React, { useEffect, useState } from 'react'
import { ToastContainer, toast } from 'react-toastify'
import { toast } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { useLocation } from 'react-router-dom'
@@ -71,8 +71,6 @@ const AuthCode = () => {
>
<Button variant="contained">Copy to Clipboard</Button>
</CopyToClipboard>
<ToastContainer />
</Box>
)
}

View 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

View 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

View File

@@ -13,6 +13,8 @@ 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
@@ -24,6 +26,8 @@ export const AppContext = createContext<AppContextProps>({
checkingSession: false,
loggedIn: false,
setLoggedIn: null,
userId: 0,
setUserId: null,
username: '',
setUsername: null,
displayName: '',
@@ -35,6 +39,7 @@ 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('')
@@ -46,9 +51,10 @@ 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)
@@ -70,6 +76,8 @@ const AppContextProvider = (props: { children: ReactNode }) => {
checkingSession,
loggedIn,
setLoggedIn,
userId,
setUserId,
username,
setUsername,
displayName,