1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-27 00:19:39 +01:00

Feat: admin users (#266)

* fix: make it work

* fix: cleanup add/update users a bit

* fix: fix

* fix: fine tune
This commit is contained in:
Ivar Conradi Østhus 2021-04-09 13:25:39 +02:00 committed by GitHub
parent 45bce4576d
commit 5166198f07
12 changed files with 223 additions and 206 deletions

View File

@ -3,10 +3,20 @@ import { Dialog, DialogTitle, DialogActions, DialogContent, Button } from '@mate
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
const ConfirmDialogue = ({ children, open, onClick, onClose, title, primaryButtonText, secondaryButtonText }) => ( const ConfirmDialogue = ({
children,
open,
onClick,
onClose,
title,
primaryButtonText,
secondaryButtonText,
fullWidth = false,
}) => (
<Dialog <Dialog
open={open} open={open}
onClose={onClose} onClose={onClose}
fullWidth={fullWidth}
aria-labelledby={'simple-modal-title'} aria-labelledby={'simple-modal-title'}
aria-describedby={'simple-modal-description'} aria-describedby={'simple-modal-description'}
> >
@ -32,6 +42,7 @@ ConfirmDialogue.propTypes = {
ariaLabel: PropTypes.string, ariaLabel: PropTypes.string,
ariaDescription: PropTypes.string, ariaDescription: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
fullWidth: PropTypes.bool,
}; };
export default ConfirmDialogue; export default ConfirmDialogue;

View File

@ -7,10 +7,10 @@ import AddUser from '../add-user-component';
import ChangePassword from '../change-password-component'; import ChangePassword from '../change-password-component';
import UpdateUser from '../update-user-component'; import UpdateUser from '../update-user-component';
import DelUser from '../del-user-component'; import DelUser from '../del-user-component';
import { showPermissions } from '../util';
import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender'; import ConditionallyRender from '../../../../component/common/ConditionallyRender/ConditionallyRender';
function UsersList({ function UsersList({
roles,
fetchUsers, fetchUsers,
removeUser, removeUser,
addUser, addUser,
@ -68,6 +68,11 @@ function UsersList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const renderRole = roleId => {
const role = roles.find(r => r.id === roleId);
return role ? role.name : '';
}
return ( return (
<div> <div>
<Table> <Table>
@ -77,7 +82,7 @@ function UsersList({
<TableCell>Created</TableCell> <TableCell>Created</TableCell>
<TableCell>Username</TableCell> <TableCell>Username</TableCell>
<TableCell>Name</TableCell> <TableCell>Name</TableCell>
<TableCell>Access</TableCell> <TableCell>Role</TableCell>
<TableCell>{hasPermission('ADMIN') ? 'Action' : ''}</TableCell> <TableCell>{hasPermission('ADMIN') ? 'Action' : ''}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -88,7 +93,7 @@ function UsersList({
<TableCell>{formatFullDateTimeWithLocale(item.createdAt, location.locale)}</TableCell> <TableCell>{formatFullDateTimeWithLocale(item.createdAt, location.locale)}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell> <TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
<TableCell style={{ textAlign: 'left' }}>{item.name}</TableCell> <TableCell style={{ textAlign: 'left' }}>{item.name}</TableCell>
<TableCell>{showPermissions(item.permissions)}</TableCell> <TableCell>{renderRole(item.rootRole)}</TableCell>
<ConditionallyRender <ConditionallyRender
condition={hasPermission('ADMIN')} condition={hasPermission('ADMIN')}
show={ show={
@ -104,34 +109,37 @@ function UsersList({
</IconButton> </IconButton>
</TableCell> </TableCell>
} }
elseShow={ elseShow={<TableCell />}
<TableCell>
<IconButton aria-label="Change password" title="Change password" onClick={openPwDialog(item)}>
<Icon>lock</Icon>
</IconButton>
</TableCell>
}
/> />
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
<br /> <br />
<Button variant="contained" color="primary" onClick={openDialog}> <ConditionallyRender
Add new user condition={hasPermission('ADMIN')}
</Button> show={
<Button variant="contained" color="primary" onClick={openDialog}>
Add new user
</Button>
}
elseShow={<small>PS! Only admins can add/remove users.</small>}
/>
<AddUser <AddUser
showDialog={showDialog} showDialog={showDialog}
closeDialog={closeDialog} closeDialog={closeDialog}
addUser={addUser} addUser={addUser}
validatePassword={validatePassword} validatePassword={validatePassword}
roles={roles}
/> />
<UpdateUser {updateDialog.open && <UpdateUser
showDialog={updateDialog.open} showDialog={updateDialog.open}
closeDialog={closeUpdateDialog} closeDialog={closeUpdateDialog}
updateUser={updateUser} updateUser={updateUser}
user={updateDialog.user} user={updateDialog.user}
/> roles={roles}
/>}
<ChangePassword <ChangePassword
showDialog={pwDialog.open} showDialog={pwDialog.open}
closeDialog={closePwDialog} closeDialog={closePwDialog}
@ -155,6 +163,7 @@ function UsersList({
} }
UsersList.propTypes = { UsersList.propTypes = {
roles: PropTypes.array.isRequired,
users: PropTypes.array.isRequired, users: PropTypes.array.isRequired,
fetchUsers: PropTypes.func.isRequired, fetchUsers: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired, removeUser: PropTypes.func.isRequired,

View File

@ -12,6 +12,7 @@ import { hasPermission } from '../../../../permissions';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
users: state.userAdmin.toJS(), users: state.userAdmin.toJS(),
roles: state.roles.get('root').toJS() || [],
location: state.settings.toJS().location || {}, location: state.settings.toJS().location || {},
hasPermission: permission => hasPermission(state.user.get('profile'), permission), hasPermission: permission => hasPermission(state.user.get('profile'), permission),
}); });

View File

@ -1,49 +1,28 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Dialogue from '../../../component/common/Dialogue'; import Dialogue from '../../../component/common/Dialogue';
import { import UserForm from './user-form';
TextField,
DialogTitle,
DialogContent,
RadioGroup,
Radio,
FormControl,
FormControlLabel,
FormLabel,
} from '@material-ui/core';
import { trim } from '../../../component/common/util';
import commonStyles from '../../../component/common/common.module.scss';
const EMPTY = { userType: 'regular' }; function AddUser({ showDialog, closeDialog, addUser, roles }) {
const [data, setData] = useState({});
function AddUser({ showDialog, closeDialog, addUser, validatePassword }) {
const [data, setData] = useState(EMPTY);
const [error, setError] = useState({}); const [error, setError] = useState({});
const updateField = e => {
setData({
...data,
[e.target.name]: e.target.value,
});
};
const updateFieldWithTrim = e => {
setData({
...data,
[e.target.name]: trim(e.target.value),
});
};
const submit = async e => { const submit = async e => {
e.preventDefault(); e.preventDefault();
if (!data.email) { if (!data.email) {
setError({ general: 'You must specify the email adress' }); setError({ general: 'You must specify the email address' });
return;
}
if (!data.rootRole) {
setError({ general: 'You must specify a role for the user' });
return; return;
} }
try { try {
await addUser(data); await addUser(data);
setData(EMPTY); setData({});
setError({});
closeDialog(); closeDialog();
} catch (error) { } catch (error) {
const msg = error.message || 'Could not create user'; const msg = error.message || 'Could not create user';
@ -51,22 +30,10 @@ function AddUser({ showDialog, closeDialog, addUser, validatePassword }) {
} }
}; };
const onPasswordBlur = async e => {
e.preventDefault();
setError({ password: '' });
if (data.password) {
try {
await validatePassword(data.password);
} catch (error) {
const msg = error.message || '';
setError({ password: msg });
}
}
};
const onCancel = e => { const onCancel = e => {
e.preventDefault(); e.preventDefault();
setData(EMPTY); setData({});
setError({});
closeDialog(); closeDialog();
}; };
@ -79,61 +46,9 @@ function AddUser({ showDialog, closeDialog, addUser, validatePassword }) {
onClose={onCancel} onClose={onCancel}
primaryButtonText="Add user" primaryButtonText="Add user"
secondaryButtonText="Cancel" secondaryButtonText="Cancel"
fullWidth
> >
<form onSubmit={submit}> <UserForm title="Add new user" data={data} setData={setData} roles={roles} submit={submit} error={error} />
<DialogTitle>Add new user</DialogTitle>
<DialogContent
className={commonStyles.contentSpacing}
style={{ display: 'flex', flexDirection: 'column' }}
>
<p style={{ color: 'red' }}>{error.general}</p>
<TextField
label="Full name"
name="name"
value={data.name}
error={error.name !== undefined}
helperText={error.name}
type="name"
variant="outlined"
size="small"
onChange={updateField}
/>
<TextField
label="Email"
name="email"
value={data.email}
error={error.email !== undefined}
helperText={error.email}
type="email"
variant="outlined"
size="small"
onChange={updateFieldWithTrim}
/>
<TextField
label="Password"
name="password"
type="password"
value={data.password}
error={error.password !== undefined}
helperText={error.password}
variant="outlined"
size="small"
onChange={updateField}
onBlur={onPasswordBlur}
/>
<br />
<br />
<FormControl>
<FormLabel component="legend">User type</FormLabel>
<RadioGroup name="userType" value={data.userType} onChange={updateField}>
<FormControlLabel label="Regular" control={<Radio />} value="regular" />
<FormControlLabel label="Admin" control={<Radio />} value="admin" />
<FormControlLabel label="Read-only" control={<Radio />} value="read" />
</RadioGroup>
</FormControl>
</DialogContent>
</form>
</Dialogue> </Dialogue>
); );
} }
@ -143,6 +58,7 @@ AddUser.propTypes = {
closeDialog: PropTypes.func.isRequired, closeDialog: PropTypes.func.isRequired,
addUser: PropTypes.func.isRequired, addUser: PropTypes.func.isRequired,
validatePassword: PropTypes.func.isRequired, validatePassword: PropTypes.func.isRequired,
roles: PropTypes.array.isRequired,
}; };
export default AddUser; export default AddUser;

View File

@ -4,6 +4,7 @@ import { TextField, DialogTitle, DialogContent } from '@material-ui/core';
import { trim } from '../../../component/common/util'; import { trim } from '../../../component/common/util';
import { modalStyles } from './util'; import { modalStyles } from './util';
import Dialogue from '../../../component/common/Dialogue/Dialogue'; import Dialogue from '../../../component/common/Dialogue/Dialogue';
import commonStyles from '../../../component/common/common.module.scss';
function ChangePassword({ showDialog, closeDialog, changePassword, validatePassword, user = {} }) { function ChangePassword({ showDialog, closeDialog, changePassword, validatePassword, user = {} }) {
const [data, setData] = useState({}); const [data, setData] = useState({});
@ -67,11 +68,14 @@ function ChangePassword({ showDialog, closeDialog, changePassword, validatePassw
> >
<form onSubmit={submit}> <form onSubmit={submit}>
<DialogTitle>Update password</DialogTitle> <DialogTitle>Update password</DialogTitle>
<DialogContent> <DialogContent
className={commonStyles.contentSpacing}
style={{ display: 'flex', flexDirection: 'column' }}
>
<p>User: {user.username || user.email}</p> <p>User: {user.username || user.email}</p>
<p style={{ color: 'red' }}>{error.general}</p> <p style={{ color: 'red' }}>{error.general}</p>
<TextField <TextField
label="New passord" label="New password"
name="password" name="password"
type="password" type="password"
value={data.password} value={data.password}
@ -83,7 +87,7 @@ function ChangePassword({ showDialog, closeDialog, changePassword, validatePassw
size="small" size="small"
/> />
<TextField <TextField
label="Confirm passord" label="Confirm password"
name="confirm" name="confirm"
type="password" type="password"
value={data.confirm} value={data.confirm}

View File

@ -1,99 +1,60 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import Dialogue from '../../../component/common/Dialogue';
Button, import UserForm from './user-form';
TextField,
DialogTitle,
DialogContent,
DialogActions,
RadioGroup,
Radio,
Modal,
} from '@material-ui/core';
import { showPermissions, modalStyles } from './util';
function AddUser({ user, showDialog, closeDialog, updateUser }) { function AddUser({ user = {}, showDialog, closeDialog, updateUser, roles }) {
const [data, setData] = useState(user); const [data, setData] = useState({});
const [error, setError] = useState({}); const [error, setError] = useState({});
useEffect(() => {
setData({
id: user.id,
email: user.email || '',
rootRole: user.rootRole || '',
name: user.name || '',
});
}, [user])
if (!user) { if (!user) {
return null; return null;
} }
const updateField = e => {
setData({
...data,
[e.target.name]: e.target.value,
});
};
const submit = async e => { const submit = async e => {
e.preventDefault(); e.preventDefault();
try { try {
await updateUser(data); await updateUser(data);
setData({});
setError({});
closeDialog(); closeDialog();
} catch (error) { } catch (error) {
setError({ general: 'Could not create user' }); setError({ general: 'Could not update user' });
} }
}; };
const onCancel = e => { const onCancel = e => {
e.preventDefault(); e.preventDefault();
setData({});
setError({});
closeDialog(); closeDialog();
}; };
const userType = data.userType || showPermissions(user.permissions);
return ( return (
<Modal open={showDialog} style={modalStyles} onClose={onCancel}> <Dialogue
<form onSubmit={submit}> onClick={e => {
<DialogTitle>Edit user</DialogTitle> submit(e);
}}
<DialogContent> open={showDialog}
<p>{error.general}</p> onClose={onCancel}
<TextField primaryButtonText="Update user"
label="Full name" secondaryButtonText="Cancel"
name="name" fullWidth
value={data.name} >
error={error.name} <UserForm title="Update user" data={data} setData={setData} roles={roles} submit={submit} error={error} />
type="name" </Dialogue>
onChange={updateField}
/>
<TextField
label="Email"
name="email"
contentEditable="false"
editable="false"
readOnly
value={data.email}
type="email"
/>
<br />
<br />
<RadioGroup name="userType" value={userType} onChange={updateField} childContainer="div">
<Radio value="regular" ripple>
Regular user
</Radio>
<Radio value="admin" ripple>
Admin user
</Radio>
<Radio value="read" ripple>
Read only
</Radio>
</RadioGroup>
</DialogContent>
<DialogActions>
<Button raised colored type="submit">
Update
</Button>
<Button type="button" onClick={onCancel}>
Cancel
</Button>
</DialogActions>
</form>
</Modal>
); );
} }
@ -102,6 +63,7 @@ AddUser.propTypes = {
closeDialog: PropTypes.func.isRequired, closeDialog: PropTypes.func.isRequired,
updateUser: PropTypes.func.isRequired, updateUser: PropTypes.func.isRequired,
user: PropTypes.object, user: PropTypes.object,
roles: PropTypes.array.isRequired,
}; };
export default AddUser; export default AddUser;

View File

@ -0,0 +1,103 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
TextField,
DialogTitle,
DialogContent,
RadioGroup,
Radio,
FormControl,
FormLabel,
FormControlLabel,
} from '@material-ui/core';
import commonStyles from '../../../component/common/common.module.scss';
import { trim } from '../../../component/common/util';
function UserForm({ title, submit, data, error, setData, roles }) {
const updateField = e => {
setData({
...data,
[e.target.name]: e.target.value,
});
};
const updateFieldWithTrim = e => {
setData({
...data,
[e.target.name]: trim(e.target.value),
});
};
const updateNumberField = e => {
setData({
...data,
[e.target.name]: +e.target.value,
});
};
return (
<form onSubmit={submit}>
<DialogTitle>{title}</DialogTitle>
<DialogContent className={commonStyles.contentSpacing} style={{ display: 'flex', flexDirection: 'column' }}>
<p style={{ color: 'red' }}>{error.general}</p>
<TextField
label="Full name"
name="name"
value={data.name || ''}
error={error.name !== undefined}
helperText={error.name}
type="name"
variant="outlined"
size="small"
onChange={updateField}
/>
<TextField
label="Email"
name="email"
required
value={data.email || ''}
error={error.email !== undefined}
helperText={error.email}
variant="outlined"
size="small"
type="email"
onChange={updateFieldWithTrim}
/>
<br />
<br />
<FormControl>
<FormLabel component="legend">Role</FormLabel>
<RadioGroup name="rootRole" value={data.rootRole || ''} onChange={updateNumberField}>
{roles.map(role => (
<FormControlLabel
key={`role-${role.id}`}
labelPlacement="end"
style={{ margin: '3px 0', border: '1px solid #EFEFEF' }}
label={
<div>
<strong>{role.name}</strong>
<p>{role.description}</p>
</div>
}
control={<Radio />}
value={role.id}
/>
))}
</RadioGroup>
</FormControl>
</DialogContent>
</form>
);
}
UserForm.propTypes = {
title: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
error: PropTypes.object.isRequired,
submit: PropTypes.func.isRequired,
setData: PropTypes.func.isRequired,
roles: PropTypes.array.isRequired,
};
export default UserForm;

View File

@ -1,13 +1,3 @@
export const showPermissions = permissions => {
if (!permissions || permissions.length === 0) {
return 'read';
} else if (permissions.includes('ADMIN')) {
return 'admin';
} else {
return 'regular';
}
};
export const modalStyles = { export const modalStyles = {
overlay: { overlay: {
position: 'absolute', position: 'absolute',

View File

@ -1,7 +1,7 @@
import api from './api'; import api from './api';
import { dispatchError } from '../util'; import { dispatchError } from '../util';
export const START_FETCH_USERS = 'START_FETCH_USERS'; export const START_FETCH_USERS = 'START_FETCH_USERS';
export const RECIEVE_USERS = 'RECIEVE_USERS'; export const RECEIVE_USERS = 'RECEIVE_USERS';
export const ERROR_FETCH_USERS = 'ERROR_FETCH_USERS'; export const ERROR_FETCH_USERS = 'ERROR_FETCH_USERS';
export const REMOVE_USER = 'REMOVE_USER'; export const REMOVE_USER = 'REMOVE_USER';
export const REMOVE_USER_ERROR = 'REMOVE_USER_ERROR'; export const REMOVE_USER_ERROR = 'REMOVE_USER_ERROR';
@ -15,7 +15,7 @@ export const VALIDATE_PASSWORD_ERROR = 'VALIDATE_PASSWORD_ERROR';
const debug = require('debug')('unleash:e-user-admin-actions'); const debug = require('debug')('unleash:e-user-admin-actions');
const gotUsers = value => ({ const gotUsers = value => ({
type: RECIEVE_USERS, type: RECEIVE_USERS,
value, value,
}); });

View File

@ -1,10 +1,10 @@
import { List } from 'immutable'; import { List } from 'immutable';
import { RECIEVE_USERS, ADD_USER, REMOVE_USER, UPDATE_USER } from './actions'; import { RECEIVE_USERS, ADD_USER, REMOVE_USER, UPDATE_USER } from './actions';
const store = (state = new List(), action) => { const store = (state = new List(), action) => {
switch (action.type) { switch (action.type) {
case RECIEVE_USERS: case RECEIVE_USERS:
return new List(action.value); return new List(action.value.users);
case ADD_USER: case ADD_USER:
return state.push(action.user); return state.push(action.user);
case UPDATE_USER: case UPDATE_USER:

View File

@ -0,0 +1,19 @@
import { List, fromJS } from 'immutable';
import { RECEIVE_USERS } from './actions';
function getInitialState() {
return fromJS({
root: [],
});
}
const store = (state = getInitialState(), action) => {
switch (action.type) {
case RECEIVE_USERS:
return state.set('root', new List(action.value.rootRoles));
default:
return state;
}
};
export default store;

View File

@ -17,6 +17,7 @@ import context from './context';
import projects from './project'; import projects from './project';
import addons from './addons'; import addons from './addons';
import userAdmin from './e-user-admin'; import userAdmin from './e-user-admin';
import roles from './e-user-admin/roles-store';
import apiAdmin from './e-api-admin'; import apiAdmin from './e-api-admin';
import authAdmin from './e-admin-auth'; import authAdmin from './e-admin-auth';
import apiCalls from './api-calls'; import apiCalls from './api-calls';
@ -40,6 +41,7 @@ const unleashStore = combineReducers({
projects, projects,
addons, addons,
userAdmin, userAdmin,
roles,
apiAdmin, apiAdmin,
authAdmin, authAdmin,
apiCalls, apiCalls,