mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: add edit and create user screen (NEW) (#601)
* feat: add edit and create user screen * refactor: rename create user component * fix: add missing documentation link * fix: remove unused dependencies * feat: add confirm screen * refactor: change UserForm and delete unused components * refactor: remove toast when create new user * fix: add margin top to form elements Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
1b097f85d6
commit
80e80805f7
@ -1,93 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import Dialogue from '../../../common/Dialogue';
|
||||
|
||||
import { IUserApiErrors } from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
||||
import IRole from '../../../../interfaces/role';
|
||||
import AddUserForm from './AddUserForm/AddUserForm';
|
||||
|
||||
interface IAddUserProps {
|
||||
showDialog: boolean;
|
||||
closeDialog: () => void;
|
||||
addUser: (data: any) => any;
|
||||
validatePassword: () => boolean;
|
||||
userLoading: boolean;
|
||||
userApiErrors: IUserApiErrors;
|
||||
roles: IRole[];
|
||||
}
|
||||
|
||||
interface IAddUserFormData {
|
||||
name: string;
|
||||
email: string;
|
||||
rootRole: number;
|
||||
sendEmail: boolean;
|
||||
}
|
||||
|
||||
const EDITOR_ROLE_ID = 2;
|
||||
|
||||
const initialData = { email: '', name: '', rootRole: EDITOR_ROLE_ID, sendEmail: true };
|
||||
|
||||
const AddUser = ({
|
||||
showDialog,
|
||||
closeDialog,
|
||||
userLoading,
|
||||
addUser,
|
||||
userApiErrors,
|
||||
roles,
|
||||
}: IAddUserProps) => {
|
||||
const [data, setData] = useState<IAddUserFormData>(initialData);
|
||||
const [error, setError] = useState({});
|
||||
|
||||
const submit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!data.email) {
|
||||
setError({ general: 'You must specify the email address' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.rootRole) {
|
||||
setError({ general: 'You must specify a role for the user' });
|
||||
return;
|
||||
}
|
||||
|
||||
await addUser(data);
|
||||
setData(initialData);
|
||||
setError({});
|
||||
};
|
||||
|
||||
const onCancel = (e: Event) => {
|
||||
e.preventDefault();
|
||||
setData(initialData);
|
||||
setError({});
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const formId = 'add-user-dialog-form';
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
onClick={e => {
|
||||
submit(e);
|
||||
}}
|
||||
formId={formId}
|
||||
open={showDialog}
|
||||
onClose={onCancel}
|
||||
primaryButtonText="Add user"
|
||||
secondaryButtonText="Cancel"
|
||||
title="Add team member"
|
||||
fullWidth
|
||||
>
|
||||
<AddUserForm
|
||||
formId={formId}
|
||||
userApiErrors={userApiErrors}
|
||||
data={data}
|
||||
setData={setData}
|
||||
roles={roles}
|
||||
submit={submit}
|
||||
error={error}
|
||||
userLoading={userLoading}
|
||||
/>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUser;
|
@ -1,217 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
DialogContent,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Switch,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { trim } from '../../../../common/util';
|
||||
import { useCommonStyles } from '../../../../../common.styles';
|
||||
import ConditionallyRender from '../../../../common/ConditionallyRender';
|
||||
import { useStyles } from './AddUserForm.styles';
|
||||
import useLoading from '../../../../../hooks/useLoading';
|
||||
import {
|
||||
ADD_USER_ERROR,
|
||||
UPDATE_USER_ERROR,
|
||||
} from '../../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
function AddUserForm({
|
||||
submit,
|
||||
data,
|
||||
error,
|
||||
setData,
|
||||
roles,
|
||||
userLoading,
|
||||
userApiErrors,
|
||||
formId,
|
||||
}) {
|
||||
const ref = useLoading(userLoading);
|
||||
const commonStyles = useCommonStyles();
|
||||
const styles = useStyles();
|
||||
|
||||
const updateField = e => {
|
||||
setData({
|
||||
...data,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const updateFieldWithTrim = e => {
|
||||
setData({
|
||||
...data,
|
||||
[e.target.name]: trim(e.target.value),
|
||||
});
|
||||
};
|
||||
|
||||
const toggleBooleanField = e => {
|
||||
setData({
|
||||
...data,
|
||||
[e.target.name]: !data[e.target.name],
|
||||
});
|
||||
};
|
||||
|
||||
const updateNumberField = e => {
|
||||
setData({
|
||||
...data,
|
||||
[e.target.name]: +e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const sortRoles = (a, b) => {
|
||||
if (b.name[0] < a.name[0]) {
|
||||
return 1;
|
||||
} else if (a.name[0] < b.name[0]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const apiError =
|
||||
userApiErrors[ADD_USER_ERROR] || userApiErrors[UPDATE_USER_ERROR];
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<form id={formId} onSubmit={submit}>
|
||||
<DialogContent>
|
||||
<ConditionallyRender
|
||||
condition={apiError}
|
||||
show={
|
||||
<Alert
|
||||
className={styles.errorAlert}
|
||||
severity="error"
|
||||
data-loading
|
||||
>
|
||||
{apiError}
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className={classnames(
|
||||
commonStyles.contentSpacingY,
|
||||
commonStyles.flexColumn,
|
||||
styles.userInfoContainer
|
||||
)}
|
||||
>
|
||||
<Typography variant="subtitle1" data-loading>
|
||||
Who is your team member?
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={error.general}
|
||||
show={
|
||||
<p data-loading style={{ color: 'red' }}>
|
||||
{error.general}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
autoFocus
|
||||
label="Full name"
|
||||
data-loading
|
||||
name="name"
|
||||
value={data.name || ''}
|
||||
error={error.name !== undefined}
|
||||
helperText={error.name}
|
||||
type="name"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onChange={updateField}
|
||||
/>
|
||||
<TextField
|
||||
label="Email"
|
||||
data-loading
|
||||
name="email"
|
||||
required
|
||||
value={data.email || ''}
|
||||
error={error.email !== undefined}
|
||||
helperText={error.email}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
type="email"
|
||||
onChange={updateFieldWithTrim}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
className={styles.roleSubtitle}
|
||||
data-loading
|
||||
>
|
||||
What is your team member allowed to do?
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
name="rootRole"
|
||||
value={data.rootRole || ''}
|
||||
onChange={updateNumberField}
|
||||
data-loading
|
||||
>
|
||||
{roles.sort(sortRoles).map(role => (
|
||||
<FormControlLabel
|
||||
key={`role-${role.id}`}
|
||||
labelPlacement="end"
|
||||
className={styles.roleBox}
|
||||
label={
|
||||
<div>
|
||||
<strong>{role.name}</strong>
|
||||
<Typography variant="body2">
|
||||
{role.description}
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
control={
|
||||
<Radio
|
||||
checked={role.id === data.rootRole}
|
||||
className={styles.roleRadio}
|
||||
/>
|
||||
}
|
||||
value={role.id}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<br />
|
||||
<br />
|
||||
<div className={commonStyles.flexRow}>
|
||||
<FormControl>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
className={styles.roleSubtitle}
|
||||
data-loading
|
||||
>
|
||||
Should we send an email to your new team member
|
||||
</Typography>
|
||||
<div className={commonStyles.flexRow}>
|
||||
<Switch
|
||||
name="sendEmail"
|
||||
onChange={toggleBooleanField}
|
||||
checked={data.sendEmail}
|
||||
/>
|
||||
<Typography>
|
||||
{data.sendEmail ? 'Yes' : 'No'}
|
||||
</Typography>
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AddUserForm.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
error: PropTypes.object.isRequired,
|
||||
submit: PropTypes.func.isRequired,
|
||||
setData: PropTypes.func.isRequired,
|
||||
roles: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default AddUserForm;
|
@ -1,21 +0,0 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
roleBox: {
|
||||
margin: '3px 0',
|
||||
border: '1px solid #EFEFEF',
|
||||
padding: '1rem',
|
||||
},
|
||||
userInfoContainer: {
|
||||
margin: '-20px 0',
|
||||
},
|
||||
roleRadio: {
|
||||
marginRight: '15px',
|
||||
},
|
||||
roleSubtitle: {
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
errorAlert: {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
}));
|
119
frontend/src/component/admin/users/CreateUser/CreateUser.tsx
Normal file
119
frontend/src/component/admin/users/CreateUser/CreateUser.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import UserForm from '../UserForm/UserForm';
|
||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
import useAddUserForm from '../hooks/useAddUserForm';
|
||||
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
||||
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
|
||||
import { useState } from 'react';
|
||||
import { scrollToTop } from '../../../common/util';
|
||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||
|
||||
const CreateUser = () => {
|
||||
/* @ts-ignore */
|
||||
const { setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const history = useHistory();
|
||||
const {
|
||||
name,
|
||||
setName,
|
||||
email,
|
||||
setEmail,
|
||||
sendEmail,
|
||||
setSendEmail,
|
||||
rootRole,
|
||||
setRootRole,
|
||||
getAddUserPayload,
|
||||
validateName,
|
||||
validateEmail,
|
||||
errors,
|
||||
clearErrors,
|
||||
} = useAddUserForm();
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [inviteLink, setInviteLink] = useState('');
|
||||
|
||||
const { addUser, userLoading: loading } = useAdminUsersApi();
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
clearErrors();
|
||||
const validName = validateName();
|
||||
const validEmail = validateEmail();
|
||||
|
||||
if (validName && validEmail) {
|
||||
const payload = getAddUserPayload();
|
||||
try {
|
||||
await addUser(payload)
|
||||
.then(res => res.json())
|
||||
.then(user => {
|
||||
scrollToTop();
|
||||
setInviteLink(user.inviteLink);
|
||||
setShowConfirm(true);
|
||||
});
|
||||
} catch (e: any) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
const closeConfirm = () => {
|
||||
setShowConfirm(false);
|
||||
history.push('/admin/user-admin');
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request POST '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/user-admin' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getAddUserPayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title="Create Unleash user"
|
||||
description="In order to get access to Unleash needs to have an Unleash root role as Admin, Editor or Viewer.
|
||||
You can also add the user to projects as member or owner in the specific projects."
|
||||
documentationLink="https://docs.getunleash.io/user_guide/user-management"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<UserForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
name={name}
|
||||
setName={setName}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
sendEmail={sendEmail}
|
||||
setSendEmail={setSendEmail}
|
||||
rootRole={rootRole}
|
||||
setRootRole={setRootRole}
|
||||
clearErrors={clearErrors}
|
||||
>
|
||||
<PermissionButton
|
||||
onClick={handleSubmit}
|
||||
permission={ADMIN}
|
||||
type="submit"
|
||||
>
|
||||
Create user
|
||||
</PermissionButton>
|
||||
</UserForm>
|
||||
<ConfirmUserAdded
|
||||
open={showConfirm}
|
||||
closeConfirm={closeConfirm}
|
||||
emailSent={sendEmail}
|
||||
inviteLink={inviteLink}
|
||||
/>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUser;
|
114
frontend/src/component/admin/users/EditUser/EditUser.tsx
Normal file
114
frontend/src/component/admin/users/EditUser/EditUser.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import UserForm from '../UserForm/UserForm';
|
||||
import useAddUserForm from '../hooks/useAddUserForm';
|
||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from '../../../../hooks/useToast';
|
||||
import useAdminUsersApi from '../../../../hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
|
||||
import useUserInfo from '../../../../hooks/api/getters/useUserInfo/useUserInfo';
|
||||
import { scrollToTop } from '../../../common/util';
|
||||
import { useEffect } from 'react';
|
||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||
import { ADMIN } from '../../../providers/AccessProvider/permissions';
|
||||
import { EDIT } from '../../../../constants/misc';
|
||||
|
||||
const EditUser = () => {
|
||||
useEffect(() => {
|
||||
scrollToTop();
|
||||
}, []);
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { user, refetch } = useUserInfo(id);
|
||||
const { updateUser, userLoading: loading } = useAdminUsersApi();
|
||||
const history = useHistory();
|
||||
const {
|
||||
name,
|
||||
setName,
|
||||
email,
|
||||
setEmail,
|
||||
sendEmail,
|
||||
setSendEmail,
|
||||
rootRole,
|
||||
setRootRole,
|
||||
getAddUserPayload,
|
||||
validateName,
|
||||
errors,
|
||||
clearErrors,
|
||||
} = useAddUserForm(
|
||||
user?.name,
|
||||
user?.email,
|
||||
user?.sendEmail,
|
||||
user?.rootRole
|
||||
);
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request PUT '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/user-admin/${id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(getAddUserPayload(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const payload = getAddUserPayload();
|
||||
const validName = validateName();
|
||||
|
||||
if (validName) {
|
||||
try {
|
||||
await updateUser({ ...payload, id });
|
||||
refetch();
|
||||
history.push('/admin/users');
|
||||
setToastData({
|
||||
title: 'User information updated',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (e: any) {
|
||||
setToastApiError(e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title="Edit user"
|
||||
description="In order to get access to Unleash needs to have an Unleash root role as Admin, Editor or Viewer.
|
||||
You can also add the user to projects as member or owner in the specific projects."
|
||||
documentationLink="https://docs.getunleash.io/user_guide/user-management"
|
||||
formatApiCode={formatApiCode}
|
||||
>
|
||||
<UserForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
name={name}
|
||||
setName={setName}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
sendEmail={sendEmail}
|
||||
setSendEmail={setSendEmail}
|
||||
rootRole={rootRole}
|
||||
setRootRole={setRootRole}
|
||||
clearErrors={clearErrors}
|
||||
mode={EDIT}
|
||||
>
|
||||
<PermissionButton
|
||||
onClick={handleSubmit}
|
||||
permission={ADMIN}
|
||||
type="submit"
|
||||
>
|
||||
Edit user
|
||||
</PermissionButton>
|
||||
</UserForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUser;
|
@ -0,0 +1,68 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
maxWidth: '400px',
|
||||
},
|
||||
form: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
},
|
||||
input: { width: '100%', marginBottom: '1rem' },
|
||||
label: {
|
||||
minWidth: '300px',
|
||||
[theme.breakpoints.down(600)]: {
|
||||
minWidth: 'auto',
|
||||
},
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: 'auto',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
cancelButton: {
|
||||
marginRight: '1.5rem',
|
||||
},
|
||||
inputDescription: {
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
formHeader: {
|
||||
fontWeight: 'normal',
|
||||
marginTop: '0',
|
||||
},
|
||||
header: {
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
permissionErrorContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
errorMessage: {
|
||||
//@ts-ignore
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.error.main,
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
},
|
||||
roleBox: {
|
||||
margin: '3px 0',
|
||||
border: '1px solid #EFEFEF',
|
||||
padding: '1rem',
|
||||
},
|
||||
userInfoContainer: {
|
||||
margin: '-20px 0',
|
||||
},
|
||||
roleRadio: {
|
||||
marginRight: '15px',
|
||||
},
|
||||
roleSubtitle: {
|
||||
margin: '0.5rem 0',
|
||||
},
|
||||
errorAlert: {
|
||||
marginBottom: '1rem',
|
||||
},
|
||||
flexRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
161
frontend/src/component/admin/users/UserForm/UserForm.tsx
Normal file
161
frontend/src/component/admin/users/UserForm/UserForm.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import Input from '../../../common/Input/Input';
|
||||
import {
|
||||
FormControlLabel,
|
||||
Button,
|
||||
RadioGroup,
|
||||
FormControl,
|
||||
Typography,
|
||||
Radio,
|
||||
Switch,
|
||||
} from '@material-ui/core';
|
||||
import { useStyles } from './UserForm.styles';
|
||||
import React from 'react';
|
||||
import useUsers from '../../../../hooks/api/getters/useUsers/useUsers';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||
import { EDIT } from '../../../../constants/misc';
|
||||
|
||||
interface IUserForm {
|
||||
email: string;
|
||||
name: string;
|
||||
rootRole: number;
|
||||
sendEmail: boolean;
|
||||
setEmail: React.Dispatch<React.SetStateAction<string>>;
|
||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSendEmail: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setRootRole: React.Dispatch<React.SetStateAction<number>>;
|
||||
handleSubmit: (e: any) => void;
|
||||
handleCancel: () => void;
|
||||
errors: { [key: string]: string };
|
||||
clearErrors: () => void;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
const UserForm: React.FC<IUserForm> = ({
|
||||
children,
|
||||
email,
|
||||
name,
|
||||
rootRole,
|
||||
sendEmail,
|
||||
setEmail,
|
||||
setName,
|
||||
setSendEmail,
|
||||
setRootRole,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
errors,
|
||||
clearErrors,
|
||||
mode,
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
const { roles } = useUsers();
|
||||
|
||||
const sortRoles = (a, b) => {
|
||||
if (b.name[0] < a.name[0]) {
|
||||
return 1;
|
||||
} else if (a.name[0] < b.name[0]) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={styles.form}>
|
||||
<h3 className={styles.formHeader}>User information</h3>
|
||||
|
||||
<div className={styles.container}>
|
||||
<p className={styles.inputDescription}>
|
||||
Who is the new Unleash user?
|
||||
</p>
|
||||
<Input
|
||||
className={styles.input}
|
||||
label="Full name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onFocus={() => clearErrors()}
|
||||
/>
|
||||
<Input
|
||||
className={styles.input}
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
error={Boolean(errors.email)}
|
||||
errorText={errors.email}
|
||||
onFocus={() => clearErrors()}
|
||||
/>
|
||||
<FormControl>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
className={styles.roleSubtitle}
|
||||
data-loading
|
||||
>
|
||||
What is your team member allowed to do?
|
||||
</Typography>
|
||||
<RadioGroup
|
||||
name="rootRole"
|
||||
value={rootRole || ''}
|
||||
onChange={e => setRootRole(+e.target.value)}
|
||||
data-loading
|
||||
>
|
||||
{roles.sort(sortRoles).map(role => (
|
||||
<FormControlLabel
|
||||
key={`role-${role.id}`}
|
||||
labelPlacement="end"
|
||||
className={styles.roleBox}
|
||||
label={
|
||||
<div>
|
||||
<strong>{role.name}</strong>
|
||||
<Typography variant="body2">
|
||||
{role.description}
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
control={
|
||||
<Radio
|
||||
checked={role.id === rootRole}
|
||||
className={styles.roleRadio}
|
||||
/>
|
||||
}
|
||||
value={role.id}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<ConditionallyRender
|
||||
condition={mode !== EDIT}
|
||||
show={
|
||||
<FormControl>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
className={styles.roleSubtitle}
|
||||
data-loading
|
||||
>
|
||||
Should we send an email to your new team member
|
||||
</Typography>
|
||||
<div className={styles.flexRow}>
|
||||
<Switch
|
||||
name="sendEmail"
|
||||
onChange={() => setSendEmail(!sendEmail)}
|
||||
checked={sendEmail}
|
||||
/>
|
||||
<Typography>
|
||||
{sendEmail ? 'Yes' : 'No'}
|
||||
</Typography>
|
||||
</div>
|
||||
</FormControl>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button onClick={handleCancel} className={styles.cancelButton}>
|
||||
Cancel
|
||||
</Button>
|
||||
{children}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserForm;
|
@ -13,6 +13,7 @@ import { formatDateWithLocale } from '../../../../common/util';
|
||||
import AccessContext from '../../../../../contexts/AccessContext';
|
||||
import { IUser } from '../../../../../interfaces/user';
|
||||
import { useStyles } from './UserListItem.styles';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
interface IUserListItemProps {
|
||||
user: IUser;
|
||||
@ -36,6 +37,7 @@ const UserListItem = ({
|
||||
location,
|
||||
}: IUserListItemProps) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const history = useHistory()
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
@ -78,7 +80,7 @@ const UserListItem = ({
|
||||
data-loading
|
||||
aria-label="Edit"
|
||||
title="Edit"
|
||||
onClick={openUpdateDialog(user)}
|
||||
onClick={()=> history.push(`/admin/users/${user.id}/edit`)}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
|
@ -8,9 +8,7 @@ import {
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import AddUser from '../AddUser/AddUser';
|
||||
import ChangePassword from '../change-password-component';
|
||||
import UpdateUser from '../update-user-component';
|
||||
import DelUser from '../del-user-component';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||
import AccessContext from '../../../../contexts/AccessContext';
|
||||
@ -27,9 +25,7 @@ import PaginateUI from '../../../common/PaginateUI/PaginateUI';
|
||||
function UsersList({ location, closeDialog, showDialog }) {
|
||||
const { users, roles, refetch, loading } = useUsers();
|
||||
const {
|
||||
addUser,
|
||||
removeUser,
|
||||
updateUser,
|
||||
changePassword,
|
||||
validatePassword,
|
||||
userLoading,
|
||||
@ -42,7 +38,6 @@ function UsersList({ location, closeDialog, showDialog }) {
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
const [inviteLink, setInviteLink] = useState('');
|
||||
const [delUser, setDelUser] = useState();
|
||||
const [updateDialog, setUpdateDialog] = useState({ open: false });
|
||||
const ref = useLoading(loading);
|
||||
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
||||
usePagination(users, 50);
|
||||
@ -66,28 +61,6 @@ function UsersList({ location, closeDialog, showDialog }) {
|
||||
setPwDialog({ open: false });
|
||||
};
|
||||
|
||||
const openUpdateDialog = user => e => {
|
||||
e.preventDefault();
|
||||
setUpdateDialog({ open: true, user });
|
||||
};
|
||||
|
||||
const closeUpdateDialog = () => {
|
||||
setUpdateDialog({ open: false });
|
||||
};
|
||||
|
||||
const onAddUser = data => {
|
||||
addUser(data)
|
||||
.then(res => res.json())
|
||||
.then(user => {
|
||||
setEmailSent(user.emailSent);
|
||||
setInviteLink(user.inviteLink);
|
||||
closeDialog();
|
||||
refetch();
|
||||
setShowConfirm(true);
|
||||
})
|
||||
.catch(handleCatch);
|
||||
};
|
||||
|
||||
const onDeleteUser = () => {
|
||||
removeUser(delUser)
|
||||
.then(() => {
|
||||
@ -97,15 +70,6 @@ function UsersList({ location, closeDialog, showDialog }) {
|
||||
.catch(handleCatch);
|
||||
};
|
||||
|
||||
const onUpdateUser = data => {
|
||||
updateUser(data)
|
||||
.then(() => {
|
||||
refetch();
|
||||
closeUpdateDialog();
|
||||
})
|
||||
.catch(handleCatch);
|
||||
};
|
||||
|
||||
const handleCatch = () =>
|
||||
console.log('An exception was thrown and handled.');
|
||||
|
||||
@ -126,7 +90,6 @@ function UsersList({ location, closeDialog, showDialog }) {
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
openUpdateDialog={openUpdateDialog}
|
||||
openPwDialog={openPwDialog}
|
||||
openDelDialog={openDelDialog}
|
||||
location={location}
|
||||
@ -140,7 +103,6 @@ function UsersList({ location, closeDialog, showDialog }) {
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
openUpdateDialog={openUpdateDialog}
|
||||
openPwDialog={openPwDialog}
|
||||
openDelDialog={openDelDialog}
|
||||
location={location}
|
||||
@ -185,26 +147,6 @@ function UsersList({ location, closeDialog, showDialog }) {
|
||||
inviteLink={inviteLink}
|
||||
/>
|
||||
|
||||
<AddUser
|
||||
showDialog={showDialog}
|
||||
closeDialog={closeDialog}
|
||||
addUser={onAddUser}
|
||||
userLoading={userLoading}
|
||||
validatePassword={validatePassword}
|
||||
userApiErrors={userApiErrors}
|
||||
roles={roles}
|
||||
/>
|
||||
|
||||
<UpdateUser
|
||||
showDialog={updateDialog.open}
|
||||
closeDialog={closeUpdateDialog}
|
||||
updateUser={onUpdateUser}
|
||||
userLoading={userLoading}
|
||||
userApiErrors={userApiErrors}
|
||||
user={updateDialog.user}
|
||||
roles={roles}
|
||||
/>
|
||||
|
||||
<ChangePassword
|
||||
showDialog={pwDialog.open}
|
||||
closeDialog={closePwDialog}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Typography, Avatar } from '@material-ui/core';
|
||||
import { TextField, Typography, Avatar } from '@material-ui/core';
|
||||
import { trim } from '../../common/util';
|
||||
import { modalStyles } from './util';
|
||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
||||
@ -10,7 +10,6 @@ import { useCommonStyles } from '../../../common.styles';
|
||||
import PasswordMatcher from '../../user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import PasswordField from '../../common/PasswordField/PasswordField';
|
||||
|
||||
function ChangePassword({
|
||||
showDialog,
|
||||
@ -110,20 +109,26 @@ function ChangePassword({
|
||||
/>
|
||||
|
||||
<p style={{ color: 'red' }}>{error.general}</p>
|
||||
<PasswordField
|
||||
<TextField
|
||||
label="New password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={data.password}
|
||||
helperText={error.password}
|
||||
onChange={updateField}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<PasswordField
|
||||
<TextField
|
||||
label="Confirm password"
|
||||
name="confirm"
|
||||
type="password"
|
||||
value={data.confirm}
|
||||
error={error.confirm !== undefined}
|
||||
helperText={error.confirm}
|
||||
onChange={updateField}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<PasswordMatcher
|
||||
started={data.password && data.confirm}
|
||||
|
84
frontend/src/component/admin/users/hooks/useAddUserForm.ts
Normal file
84
frontend/src/component/admin/users/hooks/useAddUserForm.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import useUsers from '../../../../hooks/api/getters/useUsers/useUsers';
|
||||
|
||||
const useProjectRoleForm = (
|
||||
initialName = '',
|
||||
initialEmail = '',
|
||||
initialSendEmail = false,
|
||||
initialRootRole = 1
|
||||
) => {
|
||||
const [name, setName] = useState(initialName);
|
||||
const [email, setEmail] = useState(initialEmail);
|
||||
const [sendEmail, setSendEmail] = useState(initialSendEmail);
|
||||
const [rootRole, setRootRole] = useState(initialRootRole);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const { users } = useUsers();
|
||||
|
||||
useEffect(() => {
|
||||
setName(initialName);
|
||||
}, [initialName]);
|
||||
|
||||
useEffect(() => {
|
||||
setEmail(initialEmail);
|
||||
}, [initialEmail]);
|
||||
|
||||
useEffect(() => {
|
||||
setSendEmail(initialSendEmail);
|
||||
}, [initialSendEmail]);
|
||||
|
||||
useEffect(() => {
|
||||
setRootRole(initialRootRole);
|
||||
}, [initialRootRole]);
|
||||
|
||||
const getAddUserPayload = () => {
|
||||
return {
|
||||
name: name,
|
||||
email: email,
|
||||
sendEmail: sendEmail,
|
||||
rootRole: rootRole,
|
||||
};
|
||||
};
|
||||
|
||||
const validateName = () => {
|
||||
if (name.length === 0) {
|
||||
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
|
||||
return false;
|
||||
}
|
||||
if (email.length === 0) {
|
||||
setErrors(prev => ({ ...prev, email: 'Email can not be empty.' }));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateEmail = () => {
|
||||
if (users.some(user => user['email'] === email)) {
|
||||
setErrors(prev => ({ ...prev, email: 'Email already exists' }));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const clearErrors = () => {
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
return {
|
||||
name,
|
||||
setName,
|
||||
email,
|
||||
setEmail,
|
||||
sendEmail,
|
||||
setSendEmail,
|
||||
rootRole,
|
||||
setRootRole,
|
||||
getAddUserPayload,
|
||||
validateName,
|
||||
validateEmail,
|
||||
clearErrors,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectRoleForm;
|
@ -40,7 +40,9 @@ const UsersAdmin = ({ history }) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={openDialog}
|
||||
onClick={() =>
|
||||
history.push('/admin/create-user')
|
||||
}
|
||||
>
|
||||
Add new user
|
||||
</Button>
|
||||
|
@ -1,84 +0,0 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dialogue from '../../common/Dialogue';
|
||||
import UserForm from './AddUser/AddUserForm/AddUserForm';
|
||||
|
||||
function AddUser({
|
||||
user,
|
||||
showDialog,
|
||||
closeDialog,
|
||||
updateUser,
|
||||
roles,
|
||||
userApiErrors,
|
||||
userLoading,
|
||||
}) {
|
||||
const [data, setData] = useState({});
|
||||
const [error, setError] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setData({
|
||||
...user,
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const submit = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await updateUser(data);
|
||||
setData({});
|
||||
setError({});
|
||||
} catch (error) {
|
||||
setError({ general: 'Could not update user' });
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = e => {
|
||||
e.preventDefault();
|
||||
setData({});
|
||||
setError({});
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
const formId = 'update-user-dialog-form'
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
onClick={e => {
|
||||
submit(e);
|
||||
}}
|
||||
formId={formId}
|
||||
open={showDialog}
|
||||
onClose={onCancel}
|
||||
primaryButtonText="Update user"
|
||||
secondaryButtonText="Cancel"
|
||||
title="Update team member"
|
||||
fullWidth
|
||||
>
|
||||
<UserForm
|
||||
data={data}
|
||||
setData={setData}
|
||||
roles={roles}
|
||||
submit={submit}
|
||||
error={error}
|
||||
userApiErrors={userApiErrors}
|
||||
userLoading={userLoading}
|
||||
formId={formId}
|
||||
/>
|
||||
</Dialogue>
|
||||
);
|
||||
}
|
||||
|
||||
AddUser.propTypes = {
|
||||
showDialog: PropTypes.bool.isRequired,
|
||||
closeDialog: PropTypes.func.isRequired,
|
||||
updateUser: PropTypes.func.isRequired,
|
||||
user: PropTypes.object,
|
||||
roles: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default AddUser;
|
@ -388,6 +388,15 @@ Array [
|
||||
"title": "Users",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"parent": "/admin",
|
||||
"path": "/admin/create-user",
|
||||
"title": "Users",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
|
@ -42,6 +42,8 @@ import FeatureCreate from '../feature/FeatureCreate/FeatureCreate';
|
||||
import ProjectRoles from '../admin/project-roles/ProjectRoles/ProjectRoles';
|
||||
import CreateProjectRole from '../admin/project-roles/CreateProjectRole/CreateProjectRole';
|
||||
import EditProjectRole from '../admin/project-roles/EditProjectRole/EditProjectRole';
|
||||
import CreateUser from '../admin/users/CreateUser/CreateUser';
|
||||
import EditUser from '../admin/users/EditUser/EditUser';
|
||||
import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken';
|
||||
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
||||
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
|
||||
@ -416,6 +418,15 @@ export const routes = [
|
||||
menu: {},
|
||||
flag: RE,
|
||||
},
|
||||
{
|
||||
path: '/admin/users/:id/edit',
|
||||
title: 'Edit',
|
||||
component: EditUser,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
path: '/admin/api',
|
||||
parent: '/admin',
|
||||
@ -434,6 +445,15 @@ export const routes = [
|
||||
layout: 'main',
|
||||
menu: { adminSettings: true },
|
||||
},
|
||||
{
|
||||
path: '/admin/create-user',
|
||||
parent: '/admin',
|
||||
title: 'Users',
|
||||
component: CreateUser,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/admin/auth',
|
||||
parent: '/admin',
|
||||
|
2
frontend/src/constants/misc.ts
Normal file
2
frontend/src/constants/misc.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const EDIT = 'Edit';
|
||||
export const CREATE = 'Create';
|
35
frontend/src/hooks/api/getters/useUserInfo/useUserInfo.ts
Normal file
35
frontend/src/hooks/api/getters/useUserInfo/useUserInfo.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from '../../../../utils/format-path';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
|
||||
const useUserInfo = (id: string, options: SWRConfiguration = {}) => {
|
||||
const fetcher = () => {
|
||||
const path = formatApiPath(`api/admin/user-admin/${id}`);
|
||||
return fetch(path, {
|
||||
method: 'GET',
|
||||
})
|
||||
.then(handleErrorResponses('Users'))
|
||||
.then(res => res.json());
|
||||
};
|
||||
|
||||
const { data, error } = useSWR(`api/admin/user-admin/${id}`, fetcher, options);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(`api/admin/user-admin/${id}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
user: data || {},
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUserInfo;
|
Loading…
Reference in New Issue
Block a user