diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index eba92d4a..1f76c106 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -357,7 +357,7 @@ components: autoExec: type: string description: 'User-specific auto-exec code' - example: '' + 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 diff --git a/api/src/controllers/internal/Execution.ts b/api/src/controllers/internal/Execution.ts index 9de586f8..b4d8b4c6 100644 --- a/api/src/controllers/internal/Execution.ts +++ b/api/src/controllers/internal/Execution.ts @@ -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}` diff --git a/api/src/controllers/user.ts b/api/src/controllers/user.ts index 9a875c7a..0c43b3ad 100644 --- a/api/src/controllers/user.ts +++ b/api/src/controllers/user.ts @@ -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 } } diff --git a/api/src/controllers/web.ts b/api/src/controllers/web.ts index 9b72d083..e64b3a44 100644 --- a/api/src/controllers/web.ts +++ b/api/src/controllers/web.ts @@ -97,6 +97,7 @@ const login = async ( return { loggedIn: true, user: { + id: user.id, username: user.username, displayName: user.displayName } diff --git a/api/src/model/User.ts b/api/src/model/User.ts index b16719ab..04d550e7 100644 --- a/api/src/model/User.ts +++ b/api/src/model/User.ts @@ -29,7 +29,7 @@ export interface UserPayload { isActive?: boolean /** * User-specific auto-exec code - * @example "" + * @example "" */ autoExec?: string } diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index 70fd8502..73ce4e54 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -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() diff --git a/web/src/App.tsx b/web/src/App.tsx index 92cb547d..071a8a70 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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() { + + + + ) diff --git a/web/src/components/header.tsx b/web/src/components/header.tsx index 7ea5ac96..7288d27c 100644 --- a/web/src/components/header.tsx +++ b/web/src/components/header.tsx @@ -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 ) => { @@ -46,7 +53,10 @@ const Header = (props: any) => { } const handleLogout = () => { - if (appContext.logout) appContext.logout() + if (appContext.logout) { + handleClose() + appContext.logout() + } } return ( { open={!!anchorEl} onClose={handleClose} > + + + - - ) } diff --git a/web/src/containers/Settings/index.tsx b/web/src/containers/Settings/index.tsx new file mode 100644 index 00000000..7a3f8297 --- /dev/null +++ b/web/src/containers/Settings/index.tsx @@ -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 ( + + + + + + + + + + + + + ) +} + +export default Settings diff --git a/web/src/containers/Settings/profile.tsx b/web/src/containers/Settings/profile.tsx new file mode 100644 index 00000000..7571250f --- /dev/null +++ b/web/src/containers/Settings/profile.tsx @@ -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 ? ( + + ) : ( + + + + + + + + + + + + + + + + } + label="isActive" + /> + } + label="isAdmin" + /> + + + + + + + + + + + + + + ) +} + +export default Profile diff --git a/web/src/context/appContext.tsx b/web/src/context/appContext.tsx index 6e5b72e4..76209012 100644 --- a/web/src/context/appContext.tsx +++ b/web/src/context/appContext.tsx @@ -13,6 +13,8 @@ interface AppContextProps { checkingSession: boolean loggedIn: boolean setLoggedIn: Dispatch> | null + userId: number + setUserId: Dispatch> | null username: string setUsername: Dispatch> | null displayName: string @@ -24,6 +26,8 @@ export const AppContext = createContext({ 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,