mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Feat/add new user (#273)
* chore: update changelog * chore: update changelog * fix: refactor AddUser * feat: add screens for email and copy * fix: remove interface * fix: admin constant in userlist * chore: fix changelog * feat: user data fetching with useSWR * feat: flesh out dialogues * fix: remove useRequest * refactor: remove redux for user admin * refactor: remove from store * refactor: userListItem * fix: change type * feat: add initial loading * fix: useLayoutEffeect in useLoading * fix: remove useEffect * fix: update snapshots * fix: remove status code * fix: remove roles from store
This commit is contained in:
parent
a82feadf01
commit
0ca753e7e5
@ -460,4 +460,4 @@ The latest version of this document is always available in
|
||||
|
||||
## [2.1.0] - 2017-01-20
|
||||
|
||||
- Adjust header #51 #52
|
||||
- Adjust header #51 #52
|
@ -3,7 +3,12 @@ import { makeStyles } from '@material-ui/styles';
|
||||
export const useCommonStyles = makeStyles(theme => ({
|
||||
contentSpacingY: {
|
||||
'& > *': {
|
||||
margin: '0.6rem 0',
|
||||
margin: '0.5rem 0',
|
||||
},
|
||||
},
|
||||
contentSpacingYLarge: {
|
||||
'& > *': {
|
||||
margin: '1.5rem 0',
|
||||
},
|
||||
},
|
||||
contentSpacingX: {
|
||||
@ -26,6 +31,7 @@ export const useCommonStyles = makeStyles(theme => ({
|
||||
},
|
||||
flexRow: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
flexColumn: {
|
||||
display: 'flex',
|
||||
|
@ -9,9 +9,9 @@ import { routes } from './menu/routes';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
import IUser from '../interfaces/user';
|
||||
import IAuthStatus from '../interfaces/user';
|
||||
interface IAppProps extends RouteComponentProps {
|
||||
user: IUser;
|
||||
user: IAuthStatus;
|
||||
}
|
||||
|
||||
const App = ({ location, user }: IAppProps) => {
|
||||
|
@ -144,7 +144,7 @@ exports[`renders correctly with permissions 1`] = `
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-8 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
className="MuiPaper-root makeStyles-tabNav-10 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
@ -192,7 +192,7 @@ exports[`renders correctly with permissions 1`] = `
|
||||
Application overview
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-9 PrivateTabIndicator-colorPrimary-10 MuiTabs-indicator"
|
||||
className="PrivateTabIndicator-root-11 PrivateTabIndicator-colorPrimary-12 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
@ -272,7 +272,7 @@ exports[`renders correctly with permissions 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-disabled={true}
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-13 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-disabled-15 Mui-disabled Mui-disabled Mui-disabled"
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-15 MuiSwitch-switchBase MuiSwitch-colorSecondary PrivateSwitchBase-disabled-17 Mui-disabled Mui-disabled Mui-disabled"
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
@ -290,7 +290,7 @@ exports[`renders correctly with permissions 1`] = `
|
||||
className="MuiIconButton-label"
|
||||
>
|
||||
<input
|
||||
className="PrivateSwitchBase-input-16 MuiSwitch-input"
|
||||
className="PrivateSwitchBase-input-18 MuiSwitch-input"
|
||||
disabled={true}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
|
@ -1,7 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Dialog, DialogTitle, DialogActions, DialogContent, Button } from '@material-ui/core';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Button,
|
||||
} from '@material-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import ConditionallyRender from '../ConditionallyRender/ConditionallyRender';
|
||||
import { useStyles } from './Dialogue.styles';
|
||||
|
||||
const ConfirmDialogue = ({
|
||||
children,
|
||||
@ -12,25 +19,53 @@ const ConfirmDialogue = ({
|
||||
primaryButtonText,
|
||||
secondaryButtonText,
|
||||
fullWidth = false,
|
||||
}) => (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth={fullWidth}
|
||||
aria-labelledby={'simple-modal-title'}
|
||||
aria-describedby={'simple-modal-description'}
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<ConditionallyRender condition={children} show={<DialogContent>{children}</DialogContent>} />
|
||||
}) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth={fullWidth}
|
||||
aria-labelledby={'simple-modal-title'}
|
||||
aria-describedby={'simple-modal-description'}
|
||||
>
|
||||
<DialogTitle className={styles.dialogTitle}>{title}</DialogTitle>
|
||||
<ConditionallyRender
|
||||
condition={children}
|
||||
show={
|
||||
<DialogContent className={styles.dialogContentPadding}>
|
||||
{children}
|
||||
</DialogContent>
|
||||
}
|
||||
/>
|
||||
|
||||
<DialogActions>
|
||||
<Button color="primary" onClick={onClick} autoFocus>
|
||||
{primaryButtonText || "Yes, I'm sure"}
|
||||
</Button>
|
||||
<Button onClick={onClose}>{secondaryButtonText || 'No take me back.'} </Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
<DialogActions>
|
||||
<ConditionallyRender
|
||||
condition={onClick}
|
||||
show={
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={onClick}
|
||||
autoFocus
|
||||
>
|
||||
{primaryButtonText || "Yes, I'm sure"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={onClose}
|
||||
show={
|
||||
<Button onClick={onClose}>
|
||||
{secondaryButtonText || 'No take me back.'}{' '}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
ConfirmDialogue.propTypes = {
|
||||
primaryButtonText: PropTypes.string,
|
||||
|
14
frontend/src/component/common/Dialogue/Dialogue.styles.js
Normal file
14
frontend/src/component/common/Dialogue/Dialogue.styles.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
dialogTitle: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.dialogue.title.main,
|
||||
height: '150px',
|
||||
padding: '2rem 3rem',
|
||||
clipPath: ' ellipse(130% 115px at 120% 20%)',
|
||||
},
|
||||
dialogContentPadding: {
|
||||
padding: '2rem 3rem',
|
||||
},
|
||||
}));
|
@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-16 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-15 PrivateNotchedOutline-legendNotched-16"
|
||||
className="PrivateNotchedOutline-legendLabelled-18 PrivateNotchedOutline-legendNotched-19"
|
||||
>
|
||||
<span>
|
||||
Stickiness
|
||||
|
@ -140,10 +140,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-13 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-14 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-15"
|
||||
className="PrivateNotchedOutline-legendLabelled-16"
|
||||
>
|
||||
<span>
|
||||
Project
|
||||
@ -175,7 +175,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-disabled={false}
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-17 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-18 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
@ -194,7 +194,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="PrivateSwitchBase-input-20 MuiSwitch-input"
|
||||
className="PrivateSwitchBase-input-21 MuiSwitch-input"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
@ -318,7 +318,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-21 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
className="MuiPaper-root makeStyles-tabNav-22 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
@ -366,7 +366,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Activation
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-22 PrivateTabIndicator-colorPrimary-23 MuiTabs-indicator"
|
||||
className="PrivateTabIndicator-root-23 PrivateTabIndicator-colorPrimary-24 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
|
@ -6,6 +6,7 @@ export const useStyles = makeStyles(theme => ({
|
||||
borderRadius: '3px',
|
||||
right: '100px',
|
||||
color: '#44606e',
|
||||
maxWidth: '350px',
|
||||
},
|
||||
headerContainer: { display: 'flex', padding: '0.5rem' },
|
||||
divider: {
|
||||
|
@ -1,3 +1,5 @@
|
||||
export const BAD_REQUEST = 400;
|
||||
export const OK = 200;
|
||||
export const NOT_FOUND = 404;
|
||||
export const FORBIDDEN = 403;
|
||||
export const UNAUTHORIZED = 401;
|
||||
|
167
frontend/src/hooks/useAdminUsersApi.ts
Normal file
167
frontend/src/hooks/useAdminUsersApi.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
BAD_REQUEST,
|
||||
FORBIDDEN,
|
||||
NOT_FOUND,
|
||||
OK,
|
||||
UNAUTHORIZED,
|
||||
} from '../constants/statusCodes';
|
||||
import { IUserPayload } from '../interfaces/user';
|
||||
import {
|
||||
AuthenticationError,
|
||||
ForbiddenError,
|
||||
headers,
|
||||
NotFoundError,
|
||||
} from '../store/api-helper';
|
||||
|
||||
export interface IUserApiErrors {
|
||||
addUser?: string;
|
||||
removeUser?: string;
|
||||
updateUser?: string;
|
||||
changePassword?: string;
|
||||
validatePassword?: string;
|
||||
}
|
||||
|
||||
export const ADD_USER_ERROR = 'addUser';
|
||||
export const UPDATE_USER_ERROR = 'updateUser';
|
||||
export const REMOVE_USER_ERROR = 'removeUser';
|
||||
export const CHANGE_PASSWORD_ERROR = 'changePassword';
|
||||
export const VALIDATE_PASSWORD_ERROR = 'validatePassword';
|
||||
|
||||
const useAdminUsersApi = () => {
|
||||
const [userApiErrors, setUserApiErrors] = useState({});
|
||||
const [userLoading, setUserLoading] = useState(false);
|
||||
|
||||
const defaultOptions: RequestInit = {
|
||||
headers,
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
const makeRequest = async (
|
||||
apiCaller: any,
|
||||
type: string
|
||||
): Promise<Response> => {
|
||||
setUserLoading(true);
|
||||
try {
|
||||
const res = await apiCaller();
|
||||
setUserLoading(false);
|
||||
if (res.status > 299) {
|
||||
await handleResponses(res, type);
|
||||
}
|
||||
|
||||
if (res.status === OK) {
|
||||
setUserApiErrors({});
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
setUserLoading(false);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const addUser = async (user: IUserPayload) => {
|
||||
return makeRequest(() => {
|
||||
return fetch('api/admin/user-admin', {
|
||||
...defaultOptions,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
}, 'addUser');
|
||||
};
|
||||
|
||||
const removeUser = async (user: IUserPayload) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/${user.id}`, {
|
||||
...defaultOptions,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}, 'removeUser');
|
||||
};
|
||||
|
||||
const updateUser = async (user: IUserPayload) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/${user.id}`, {
|
||||
...defaultOptions,
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
}, 'updateUser');
|
||||
};
|
||||
|
||||
const changePassword = async (user: IUserPayload, password: string) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/${user.id}/change-password`, {
|
||||
...defaultOptions,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
}, 'changePassword');
|
||||
};
|
||||
|
||||
const validatePassword = async (password: string) => {
|
||||
return makeRequest(() => {
|
||||
return fetch(`api/admin/user-admin/validate-password`, {
|
||||
...defaultOptions,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password }),
|
||||
});
|
||||
}, 'validatePassword');
|
||||
};
|
||||
|
||||
const handleResponses = async (res: Response, type: string) => {
|
||||
if (res.status === BAD_REQUEST) {
|
||||
const data = await res.json();
|
||||
|
||||
setUserApiErrors(prev => ({
|
||||
...prev,
|
||||
[type]: data[0].msg,
|
||||
}));
|
||||
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (res.status === NOT_FOUND) {
|
||||
setUserApiErrors(prev => ({
|
||||
...prev,
|
||||
[type]: 'Could not find the requested resource.',
|
||||
}));
|
||||
|
||||
throw new NotFoundError(res.status);
|
||||
}
|
||||
|
||||
if (res.status === UNAUTHORIZED) {
|
||||
const data = await res.json();
|
||||
|
||||
setUserApiErrors(prev => ({
|
||||
...prev,
|
||||
[type]: data[0].msg,
|
||||
}));
|
||||
|
||||
throw new AuthenticationError(res.status);
|
||||
}
|
||||
|
||||
if (res.status === FORBIDDEN) {
|
||||
const data = await res.json();
|
||||
|
||||
setUserApiErrors(prev => ({
|
||||
...prev,
|
||||
[type]: data[0].msg,
|
||||
}));
|
||||
|
||||
throw new ForbiddenError(res.status);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
addUser,
|
||||
updateUser,
|
||||
removeUser,
|
||||
changePassword,
|
||||
validatePassword,
|
||||
userApiErrors,
|
||||
userLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAdminUsersApi;
|
@ -1,10 +1,10 @@
|
||||
import { useEffect, createRef } from 'react';
|
||||
import { createRef, useLayoutEffect } from 'react';
|
||||
|
||||
type refElement = HTMLDivElement;
|
||||
|
||||
const useLoading = (loading: boolean) => {
|
||||
const ref = createRef<refElement>();
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
const elements = ref.current.querySelectorAll('[data-loading]');
|
||||
|
||||
|
30
frontend/src/hooks/useUsers.ts
Normal file
30
frontend/src/hooks/useUsers.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const useUsers = () => {
|
||||
const fetcher = () =>
|
||||
fetch(`api/admin/user-admin`, {
|
||||
method: 'GET',
|
||||
}).then(res => res.json());
|
||||
|
||||
const { data, error } = useSWR(`api/admin/user-admin`, fetcher);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(`api/admin/user-admin`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
users: data?.users || [],
|
||||
roles: data?.rootRoles || [],
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUsers;
|
17
frontend/src/icons/email.svg
Normal file
17
frontend/src/icons/email.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<svg width="197" height="177" viewBox="0 0 197 177" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M195.272 67.6884C195.201 67.6884 195.132 67.6668 195.074 67.6264L99.4918 1.00044C99.2009 0.798626 98.8553 0.690774 98.5013 0.691378C98.1474 0.691982 97.8021 0.801012 97.5119 1.00382L2.61779 67.6257C2.54273 67.6784 2.44984 67.6991 2.35954 67.6832C2.26924 67.6674 2.18893 67.6163 2.13629 67.5411C2.08364 67.466 2.06297 67.373 2.07881 67.2826C2.09466 67.1923 2.14573 67.1119 2.22079 67.0592L97.115 0.437299C97.5213 0.153417 98.0047 0.000817369 98.5002 2.3283e-06C98.9957 -0.000812712 99.4796 0.150195 99.8868 0.432738L195.469 67.0585C195.53 67.1006 195.575 67.1609 195.599 67.2306C195.623 67.3004 195.624 67.3758 195.602 67.4461C195.58 67.5164 195.536 67.5778 195.476 67.6214C195.417 67.6649 195.345 67.6884 195.272 67.6884L195.272 67.6884Z" fill="#3F3D56"/>
|
||||
<path d="M8.04028 70.0501L98.5953 2.87773L189.837 74.8297L103.261 126.199L56.2576 115.476L8.04028 70.0501Z" fill="#E6E6E6"/>
|
||||
<path d="M60.2251 157.928H15.4887C15.214 157.928 14.942 157.875 14.6881 157.77C14.4342 157.665 14.2035 157.511 14.0091 157.316C13.8147 157.122 13.6606 156.891 13.5554 156.637C13.4502 156.383 13.396 156.111 13.396 155.836C13.396 155.561 13.4502 155.289 13.5554 155.035C13.6606 154.781 13.8147 154.55 14.0091 154.356C14.2035 154.162 14.4342 154.008 14.6881 153.903C14.942 153.798 15.214 153.744 15.4887 153.744H60.2251C60.4998 153.744 60.7719 153.798 61.0258 153.903C61.2797 154.008 61.5104 154.162 61.7048 154.356C61.8991 154.55 62.0533 154.781 62.1585 155.035C62.2637 155.289 62.3179 155.561 62.3179 155.836C62.3179 156.111 62.2637 156.383 62.1585 156.637C62.0533 156.891 61.8991 157.122 61.7048 157.316C61.5104 157.511 61.2797 157.665 61.0258 157.77C60.7719 157.875 60.4998 157.928 60.2251 157.928Z" fill="#607D8B"/>
|
||||
<path d="M31.5392 148.934H15.4887C15.214 148.934 14.942 148.88 14.6881 148.776C14.4342 148.671 14.2035 148.516 14.0091 148.322C13.8147 148.128 13.6606 147.897 13.5554 147.643C13.4502 147.389 13.396 147.117 13.396 146.842C13.396 146.567 13.4502 146.295 13.5554 146.041C13.6606 145.787 13.8147 145.556 14.0091 145.362C14.2035 145.168 14.4342 145.014 14.6881 144.909C14.942 144.804 15.214 144.75 15.4887 144.75H31.5392C31.8139 144.75 32.0859 144.804 32.3398 144.909C32.5937 145.014 32.8244 145.168 33.0188 145.362C33.2132 145.556 33.3674 145.787 33.4726 146.041C33.5778 146.295 33.6319 146.567 33.6319 146.842C33.6319 147.117 33.5778 147.389 33.4726 147.643C33.3674 147.897 33.2132 148.128 33.0188 148.322C32.8244 148.516 32.5937 148.671 32.3398 148.776C32.0859 148.88 31.8139 148.934 31.5392 148.934Z" fill="#607D8B"/>
|
||||
<path d="M99.8687 107.464C99.5287 107.465 99.1918 107.399 98.8771 107.27L43.0291 84.0761V11.4759C43.0298 10.8339 43.2849 10.2184 43.7385 9.76446C44.192 9.31051 44.8069 9.05515 45.4484 9.0544H152.589C153.23 9.05515 153.845 9.31051 154.299 9.76446C154.752 10.2184 155.007 10.8339 155.008 11.4759V84.1267L154.903 84.1717L100.89 107.258C100.567 107.394 100.22 107.464 99.8687 107.464Z" fill="white"/>
|
||||
<path d="M99.8687 107.637C99.5061 107.637 99.1469 107.567 98.8112 107.43L42.8562 84.1916V11.4759C42.857 10.788 43.1303 10.1286 43.6163 9.64218C44.1022 9.1558 44.7611 8.88221 45.4483 8.88143H152.589C153.276 8.88221 153.935 9.1558 154.421 9.64218C154.907 10.1286 155.18 10.788 155.181 11.4759V84.2409L100.958 107.417C100.613 107.563 100.243 107.637 99.8687 107.637ZM43.5474 83.7295L99.0748 106.79C99.5928 107.001 100.173 106.997 100.688 106.781L154.49 83.7842V11.4759C154.489 10.9714 154.288 10.4879 153.932 10.1312C153.576 9.77452 153.093 9.57387 152.589 9.57328H45.4483C44.9444 9.57387 44.4612 9.77452 44.1049 10.1312C43.7485 10.4879 43.548 10.9714 43.5474 11.4759L43.5474 83.7295Z" fill="#3F3D56"/>
|
||||
<path d="M194.581 66.9965H194.512L154.835 83.9537L100.55 107.155C100.337 107.244 100.11 107.291 99.8797 107.292C99.6496 107.293 99.4215 107.249 99.2084 107.162L43.2018 83.9052L2.55059 67.0242L2.48849 66.9965H2.4193C1.77788 66.9972 1.16293 67.2526 0.70938 67.7066C0.255828 68.1605 0.000711706 68.776 0 69.418V174.579C0.000712619 175.221 0.255828 175.836 0.70938 176.29C1.16293 176.744 1.77788 176.999 2.4193 177H194.581C195.222 176.999 195.837 176.744 196.291 176.29C196.744 175.836 196.999 175.221 197 174.579V69.418C196.999 68.776 196.744 68.1605 196.291 67.7066C195.837 67.2526 195.222 66.9972 194.581 66.9965ZM196.309 174.579C196.309 175.037 196.126 175.477 195.802 175.801C195.478 176.126 195.039 176.308 194.581 176.308H2.4193C1.96104 176.308 1.52161 176.126 1.19757 175.801C0.873536 175.477 0.691411 175.037 0.691228 174.579V69.418C0.691855 68.9707 0.865197 68.541 1.17501 68.2187C1.48483 67.8963 1.90713 67.7063 2.35365 67.6884L43.2018 84.6524L98.9425 107.802C99.5459 108.047 100.222 108.044 100.823 107.791L154.835 84.7043L194.65 67.6884C195.096 67.7076 195.517 67.8982 195.826 68.2203C196.135 68.5425 196.308 68.9714 196.309 69.418V174.579Z" fill="#3F3D56"/>
|
||||
<path d="M99.3104 86.8775C97.2487 86.8813 95.2416 86.214 93.5919 84.9762L93.4896 84.8994L71.9541 68.3963C70.9567 67.6314 70.1197 66.6774 69.4907 65.5888C68.8617 64.5001 68.4531 63.2981 68.2882 62.0514C68.1233 60.8047 68.2054 59.5377 68.5297 58.3228C68.8541 57.1078 69.4143 55.9687 70.1785 54.9704C70.9426 53.9722 71.8958 53.1344 72.9835 52.5048C74.0711 51.8753 75.2721 51.4663 76.5177 51.3013C77.7632 51.1362 79.0291 51.2184 80.243 51.543C81.4569 51.8676 82.595 52.4284 83.5923 53.1932L97.5414 63.8988L130.505 20.8706C131.27 19.8726 132.223 19.0351 133.311 18.4059C134.399 17.7767 135.6 17.3682 136.846 17.2036C138.091 17.039 139.357 17.1216 140.571 17.4467C141.785 17.7718 142.923 18.333 143.92 19.0982L143.715 19.3767L143.925 19.1023C145.936 20.6494 147.252 22.9319 147.584 25.449C147.916 27.9661 147.237 30.5122 145.696 32.5287L106.922 83.1402C106.026 84.3065 104.872 85.2503 103.552 85.8984C102.232 86.5466 100.781 86.8816 99.3104 86.8775Z" fill="#607D8B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="197" height="177" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 5.8 KiB |
9
frontend/src/interfaces/role.ts
Normal file
9
frontend/src/interfaces/role.ts
Normal file
@ -0,0 +1,9 @@
|
||||
interface IRole {
|
||||
id: number;
|
||||
name: string;
|
||||
project: string | null;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export default IRole;
|
@ -1,7 +1,7 @@
|
||||
interface IUser {
|
||||
interface IAuthStatus {
|
||||
authDetails: IAuthDetails;
|
||||
showDialog: boolean;
|
||||
profile?: IProfile;
|
||||
profile?: IUser;
|
||||
}
|
||||
|
||||
interface IAuthDetails {
|
||||
@ -11,14 +11,28 @@ interface IAuthDetails {
|
||||
options: string[];
|
||||
}
|
||||
|
||||
interface IProfile {
|
||||
export interface IUser {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
imageUrl: string;
|
||||
loginAttempts: number;
|
||||
permissions: string[];
|
||||
seenAt: string;
|
||||
username: string;
|
||||
permissions: string[] | null;
|
||||
inviteLink: string;
|
||||
rootRole: number;
|
||||
seenAt: string | null;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export default IUser;
|
||||
export interface IUserPayload {
|
||||
name: string;
|
||||
email: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface IAddedUser extends IUser {
|
||||
emailSent?: boolean;
|
||||
}
|
||||
|
||||
export default IAuthStatus;
|
||||
|
88
frontend/src/page/admin/users/AddUser/AddUser.tsx
Normal file
88
frontend/src/page/admin/users/AddUser/AddUser.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useState } from 'react';
|
||||
import Dialogue from '../../../../component/common/Dialogue';
|
||||
|
||||
import { IUserApiErrors } from '../../../../hooks/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;
|
||||
}
|
||||
|
||||
const EDITOR_ROLE_ID = 2;
|
||||
|
||||
const initialData = { email: '', name: '', rootRole: EDITOR_ROLE_ID };
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
onClick={e => {
|
||||
submit(e);
|
||||
}}
|
||||
open={showDialog}
|
||||
onClose={onCancel}
|
||||
primaryButtonText="Add user"
|
||||
secondaryButtonText="Cancel"
|
||||
title="Add team member"
|
||||
fullWidth
|
||||
>
|
||||
<AddUserForm
|
||||
userApiErrors={userApiErrors}
|
||||
data={data}
|
||||
setData={setData}
|
||||
roles={roles}
|
||||
submit={submit}
|
||||
error={error}
|
||||
userLoading={userLoading}
|
||||
/>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUser;
|
@ -0,0 +1,175 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
TextField,
|
||||
DialogContent,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { trim } from '../../../../../component/common/util';
|
||||
import { useCommonStyles } from '../../../../../common.styles';
|
||||
import ConditionallyRender from '../../../../../component/common/ConditionallyRender';
|
||||
import { useStyles } from './AddUserForm.styles';
|
||||
import useLoading from '../../../../../hooks/useLoading';
|
||||
import {
|
||||
ADD_USER_ERROR,
|
||||
UPDATE_USER_ERROR,
|
||||
} from '../../../../../hooks/useAdminUsersApi';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
function AddUserForm({
|
||||
submit,
|
||||
data,
|
||||
error,
|
||||
setData,
|
||||
roles,
|
||||
userLoading,
|
||||
userApiErrors,
|
||||
}) {
|
||||
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 updateNumberField = e => {
|
||||
setData({
|
||||
...data,
|
||||
[e.target.name]: +e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const apiError =
|
||||
userApiErrors[ADD_USER_ERROR] || userApiErrors[UPDATE_USER_ERROR];
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<form 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
|
||||
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.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>
|
||||
</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;
|
@ -0,0 +1,21 @@
|
||||
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',
|
||||
},
|
||||
}));
|
@ -0,0 +1,30 @@
|
||||
import ConfirmUserEmail from './ConfirmUserEmail/ConfirmUserEmail';
|
||||
import ConfirmUserLink from './ConfirmUserLink/ConfirmUserLink';
|
||||
|
||||
interface IConfirmUserAddedProps {
|
||||
open: boolean;
|
||||
closeConfirm: () => void;
|
||||
inviteLink: string;
|
||||
emailSent: boolean;
|
||||
}
|
||||
|
||||
const ConfirmUserAdded = ({
|
||||
open,
|
||||
closeConfirm,
|
||||
emailSent,
|
||||
inviteLink,
|
||||
}: IConfirmUserAddedProps) => {
|
||||
if (emailSent) {
|
||||
return <ConfirmUserEmail open={open} closeConfirm={closeConfirm} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmUserLink
|
||||
open={open}
|
||||
closeConfirm={closeConfirm}
|
||||
inviteLink={inviteLink}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmUserAdded;
|
@ -0,0 +1,11 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
iconContainer: {
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
},
|
||||
emailIcon: {
|
||||
margin: '2rem auto',
|
||||
},
|
||||
});
|
@ -0,0 +1,33 @@
|
||||
import { Typography } from '@material-ui/core';
|
||||
import Dialogue from '../../../../../component/common/Dialogue';
|
||||
|
||||
import { ReactComponent as EmailIcon } from '../../../../../icons/email.svg';
|
||||
import { useStyles } from './ConfirmUserEmail.styles';
|
||||
|
||||
interface IConfirmUserEmailProps {
|
||||
open: boolean;
|
||||
closeConfirm: () => void;
|
||||
}
|
||||
|
||||
const ConfirmUserEmail = ({ open, closeConfirm }: IConfirmUserEmailProps) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<Dialogue
|
||||
open={open}
|
||||
title="Team member added"
|
||||
primaryButtonText="Close"
|
||||
onClick={closeConfirm}
|
||||
>
|
||||
<Typography>
|
||||
A new team member has been added. We’ve sent an email on your
|
||||
behalf to inform them of their new account and role. No further
|
||||
steps are required.
|
||||
</Typography>
|
||||
<div className={styles.iconContainer}>
|
||||
<EmailIcon className={styles.emailIcon} />
|
||||
</div>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmUserEmail;
|
@ -0,0 +1,58 @@
|
||||
import { Typography } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { useCommonStyles } from '../../../../../common.styles';
|
||||
import Dialogue from '../../../../../component/common/Dialogue';
|
||||
import UserInviteLink from './UserInviteLink/UserInviteLink';
|
||||
|
||||
interface IConfirmUserLink {
|
||||
open: boolean;
|
||||
closeConfirm: () => void;
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
const ConfirmUserLink = ({
|
||||
open,
|
||||
closeConfirm,
|
||||
inviteLink,
|
||||
}: IConfirmUserLink) => {
|
||||
const commonStyles = useCommonStyles();
|
||||
return (
|
||||
<Dialogue
|
||||
open={open}
|
||||
onClick={closeConfirm}
|
||||
primaryButtonText="Close"
|
||||
title="Team member added"
|
||||
>
|
||||
<div className={commonStyles.contentSpacingYLarge}>
|
||||
<Typography variant="body1">
|
||||
A new team member has been added. Please provide them with
|
||||
the following link to get started:
|
||||
</Typography>
|
||||
<UserInviteLink inviteLink={inviteLink} />
|
||||
|
||||
<Typography variant="body1">
|
||||
Copy the link and send it to the user. This will allow them
|
||||
to set up their password and get started with their Unleash
|
||||
account.
|
||||
</Typography>
|
||||
<Alert severity="info">
|
||||
<Typography variant="body2">
|
||||
Want to avoid this step in the future?{' '}
|
||||
{/* TODO - ADD LINK HERE ONCE IT EXISTS*/}
|
||||
<a
|
||||
href="https://docs.getunleash.ai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
If you configure an email server for Unleash
|
||||
</a>{' '}
|
||||
we'll automatically send informational getting started
|
||||
emails to new users once you add them.
|
||||
</Typography>
|
||||
</Alert>
|
||||
</div>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmUserLink;
|
@ -0,0 +1,73 @@
|
||||
import { useState } from 'react';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import CopyIcon from '@material-ui/icons/FileCopy';
|
||||
import { Snackbar } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
interface IInviteLinkProps {
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
interface ISnackbar {
|
||||
show: boolean;
|
||||
type: 'success' | 'error';
|
||||
text: string;
|
||||
}
|
||||
|
||||
const UserInviteLink = ({ inviteLink }: IInviteLinkProps) => {
|
||||
const [snackbar, setSnackbar] = useState<ISnackbar>({
|
||||
show: false,
|
||||
type: 'success',
|
||||
text: '',
|
||||
});
|
||||
|
||||
const handleCopy = () => {
|
||||
return navigator.clipboard
|
||||
.writeText(inviteLink)
|
||||
.then(() => {
|
||||
setSnackbar({
|
||||
show: true,
|
||||
type: 'success',
|
||||
text: 'Successfully copied invite link.',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setSnackbar({
|
||||
show: true,
|
||||
type: 'error',
|
||||
text: 'Could not copy invite link.',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: '#efefef',
|
||||
padding: '2rem',
|
||||
borderRadius: '3px',
|
||||
margin: '1rem 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{inviteLink}
|
||||
<IconButton onClick={handleCopy}>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
<Snackbar
|
||||
open={snackbar.show}
|
||||
autoHideDuration={6000}
|
||||
onClose={() =>
|
||||
setSnackbar({ show: false, type: 'success', text: '' })
|
||||
}
|
||||
>
|
||||
<Alert severity={snackbar.type}>{snackbar.text}</Alert>
|
||||
</Snackbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserInviteLink;
|
@ -0,0 +1,108 @@
|
||||
import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
Avatar,
|
||||
IconButton,
|
||||
Icon,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { SyntheticEvent, useContext } from 'react';
|
||||
import { ADMIN } from '../../../../../component/AccessProvider/permissions';
|
||||
import ConditionallyRender from '../../../../../component/common/ConditionallyRender';
|
||||
import { formatDateWithLocale } from '../../../../../component/common/util';
|
||||
import AccessContext from '../../../../../contexts/AccessContext';
|
||||
import { IUser } from '../../../../../interfaces/user';
|
||||
|
||||
interface IUserListItemProps {
|
||||
user: IUser;
|
||||
renderRole: (roleId: number) => string;
|
||||
openUpdateDialog: (user: IUser) => (e: SyntheticEvent) => void;
|
||||
openPwDialog: (user: IUser) => (e: SyntheticEvent) => void;
|
||||
openDelDialog: (user: IUser) => (e: SyntheticEvent) => void;
|
||||
location: ILocation;
|
||||
}
|
||||
|
||||
interface ILocation {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const UserListItem = ({
|
||||
user,
|
||||
renderRole,
|
||||
openDelDialog,
|
||||
openPwDialog,
|
||||
openUpdateDialog,
|
||||
location,
|
||||
}: IUserListItemProps) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
|
||||
return (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Avatar
|
||||
data-loading
|
||||
variant="rounded"
|
||||
alt={user.name}
|
||||
src={user.imageUrl}
|
||||
title={`${user.name || user.email || user.username} (id: ${
|
||||
user.id
|
||||
})`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span data-loading>
|
||||
{formatDateWithLocale(user.createdAt, location.locale)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>
|
||||
<Typography variant="body2" data-loading>
|
||||
{user.name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>
|
||||
<Typography variant="body2" data-loading>
|
||||
{user.username || user.email}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Typography variant="body2" data-loading>
|
||||
{renderRole(user.rootRole)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
data-loading
|
||||
aria-label="Edit"
|
||||
title="Edit"
|
||||
onClick={openUpdateDialog(user)}
|
||||
>
|
||||
<Icon>edit</Icon>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
data-loading
|
||||
aria-label="Change password"
|
||||
title="Change password"
|
||||
onClick={openPwDialog(user)}
|
||||
>
|
||||
<Icon>lock</Icon>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
data-loading
|
||||
aria-label="Remove user"
|
||||
title="Remove user"
|
||||
onClick={openDelDialog(user)}
|
||||
>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
}
|
||||
elseShow={<TableCell />}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserListItem;
|
@ -1,33 +1,50 @@
|
||||
/* eslint-disable no-alert */
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Avatar } from '@material-ui/core';
|
||||
import { formatDateWithLocale } from '../../../../component/common/util';
|
||||
import AddUser from '../add-user-component';
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
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 '../../../../component/common/ConditionallyRender/ConditionallyRender';
|
||||
import AccessContext from '../../../../contexts/AccessContext';
|
||||
import { ADMIN } from '../../../../component/AccessProvider/permissions';
|
||||
import ConfirmUserAdded from '../ConfirmUserAdded/ConfirmUserAdded';
|
||||
import useUsers from '../../../../hooks/useUsers';
|
||||
import useAdminUsersApi from '../../../../hooks/useAdminUsersApi';
|
||||
import UserListItem from './UserListItem/UserListItem';
|
||||
import loadingData from './loadingData';
|
||||
import useLoading from '../../../../hooks/useLoading';
|
||||
|
||||
function UsersList({
|
||||
roles,
|
||||
fetchUsers,
|
||||
removeUser,
|
||||
addUser,
|
||||
updateUser,
|
||||
changePassword,
|
||||
users,
|
||||
location,
|
||||
validatePassword,
|
||||
}) {
|
||||
function UsersList({ location }) {
|
||||
const { users, roles, refetch, loading } = useUsers();
|
||||
const {
|
||||
addUser,
|
||||
removeUser,
|
||||
updateUser,
|
||||
changePassword,
|
||||
validatePassword,
|
||||
userLoading,
|
||||
userApiErrors,
|
||||
} = useAdminUsersApi();
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const [showDialog, setDialog] = useState(false);
|
||||
const [pwDialog, setPwDialog] = useState({ open: false });
|
||||
const [delDialog, setDelDialog] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
const [inviteLink, setInviteLink] = useState('');
|
||||
const [delUser, setDelUser] = useState();
|
||||
const [updateDialog, setUpdateDialog] = useState({ open: false });
|
||||
const ref = useLoading(loading);
|
||||
|
||||
const openDialog = e => {
|
||||
e.preventDefault();
|
||||
setDialog(true);
|
||||
@ -65,18 +82,85 @@ function UsersList({
|
||||
setUpdateDialog({ open: false });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
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(() => {
|
||||
refetch();
|
||||
closeDelDialog();
|
||||
})
|
||||
.catch(handleCatch);
|
||||
};
|
||||
|
||||
const onUpdateUser = data => {
|
||||
updateUser(data)
|
||||
.then(() => {
|
||||
refetch();
|
||||
closeUpdateDialog();
|
||||
})
|
||||
.catch(handleCatch);
|
||||
};
|
||||
|
||||
const handleCatch = () =>
|
||||
console.log('An exception was thrown and handled.');
|
||||
|
||||
const closeConfirm = () => {
|
||||
setShowConfirm(false);
|
||||
setEmailSent(false);
|
||||
setInviteLink('');
|
||||
};
|
||||
|
||||
const renderRole = roleId => {
|
||||
const role = roles.find(r => r.id === roleId);
|
||||
return role ? role.name : '';
|
||||
}
|
||||
};
|
||||
|
||||
const renderUsers = () => {
|
||||
if (loading) {
|
||||
return loadingData.map(user => (
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
openUpdateDialog={openUpdateDialog}
|
||||
openPwDialog={openPwDialog}
|
||||
openDelDialog={openDelDialog}
|
||||
location={location}
|
||||
renderRole={renderRole}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return users.map(user => {
|
||||
return (
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
user={user}
|
||||
openUpdateDialog={openUpdateDialog}
|
||||
openPwDialog={openPwDialog}
|
||||
openDelDialog={openDelDialog}
|
||||
location={location}
|
||||
renderRole={renderRole}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (!users) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={ref}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@ -85,63 +169,55 @@ function UsersList({
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Username</TableCell>
|
||||
<TableCell align="center">Role</TableCell>
|
||||
<TableCell align="right">{hasAccess('ADMIN') ? 'Action' : ''}</TableCell>
|
||||
<TableCell align="right">
|
||||
{hasAccess(ADMIN) ? 'Action' : ''}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map(item => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell><Avatar variant="rounded" alt={item.name} src={item.imageUrl} title={`${item.name || item.email || item.username} (id: ${item.id})`} /></TableCell>
|
||||
<TableCell>{formatDateWithLocale(item.createdAt, location.locale)}</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>{item.name}</TableCell>
|
||||
<TableCell style={{ textAlign: 'left' }}>{item.username || item.email}</TableCell>
|
||||
<TableCell align="center">{renderRole(item.rootRole)}</TableCell>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={
|
||||
<TableCell align="right">
|
||||
<IconButton aria-label="Edit" title="Edit" onClick={openUpdateDialog(item)}>
|
||||
<Icon>edit</Icon>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Change password" title="Change password" onClick={openPwDialog(item)}>
|
||||
<Icon>lock</Icon>
|
||||
</IconButton>
|
||||
<IconButton aria-label="Remove user" title="Remove user" onClick={openDelDialog(item)}>
|
||||
<Icon>delete</Icon>
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
}
|
||||
elseShow={<TableCell />}
|
||||
/>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableBody>{renderUsers()}</TableBody>
|
||||
</Table>
|
||||
<br />
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={
|
||||
<Button variant="contained" color="primary" onClick={openDialog}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={openDialog}
|
||||
>
|
||||
Add new user
|
||||
</Button>
|
||||
}
|
||||
elseShow={<small>PS! Only admins can add/remove users.</small>}
|
||||
/>
|
||||
|
||||
<ConfirmUserAdded
|
||||
open={showConfirm}
|
||||
closeConfirm={closeConfirm}
|
||||
emailSent={emailSent}
|
||||
inviteLink={inviteLink}
|
||||
/>
|
||||
|
||||
<AddUser
|
||||
showDialog={showDialog}
|
||||
closeDialog={closeDialog}
|
||||
addUser={addUser}
|
||||
addUser={onAddUser}
|
||||
userLoading={userLoading}
|
||||
validatePassword={validatePassword}
|
||||
userApiErrors={userApiErrors}
|
||||
roles={roles}
|
||||
/>
|
||||
{updateDialog.open && <UpdateUser
|
||||
|
||||
<UpdateUser
|
||||
showDialog={updateDialog.open}
|
||||
closeDialog={closeUpdateDialog}
|
||||
updateUser={updateUser}
|
||||
updateUser={onUpdateUser}
|
||||
userLoading={userLoading}
|
||||
userApiErrors={userApiErrors}
|
||||
user={updateDialog.user}
|
||||
roles={roles}
|
||||
/>}
|
||||
/>
|
||||
|
||||
<ChangePassword
|
||||
showDialog={pwDialog.open}
|
||||
closeDialog={closePwDialog}
|
||||
@ -154,10 +230,9 @@ function UsersList({
|
||||
showDialog={delDialog}
|
||||
closeDialog={closeDelDialog}
|
||||
user={delUser}
|
||||
removeUser={() => {
|
||||
removeUser(delUser);
|
||||
closeDelDialog();
|
||||
}}
|
||||
removeUser={onDeleteUser}
|
||||
userLoading={userLoading}
|
||||
userApiErrors={userApiErrors}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,27 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import UsersList from './UsersList';
|
||||
import {
|
||||
fetchUsers,
|
||||
removeUser,
|
||||
addUser,
|
||||
changePassword,
|
||||
updateUser,
|
||||
validatePassword,
|
||||
} from '../../../../store/e-user-admin/actions';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
users: state.userAdmin.toJS(),
|
||||
roles: state.roles.get('root').toJS() || [],
|
||||
location: state.settings.toJS().location || {},
|
||||
});
|
||||
|
||||
const Container = connect(mapStateToProps, {
|
||||
fetchUsers,
|
||||
removeUser,
|
||||
addUser,
|
||||
changePassword,
|
||||
updateUser,
|
||||
validatePassword,
|
||||
})(UsersList);
|
||||
const Container = connect(mapStateToProps)(UsersList);
|
||||
|
||||
export default Container;
|
||||
|
72
frontend/src/page/admin/users/UsersList/loadingData.ts
Normal file
72
frontend/src/page/admin/users/UsersList/loadingData.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { IUser } from '../../../../interfaces/user';
|
||||
|
||||
const loadingData: IUser[] = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
email: 'some-email@email.com',
|
||||
name: 'admin',
|
||||
permissions: ['ADMIN'],
|
||||
imageUrl:
|
||||
'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
|
||||
seenAt: null,
|
||||
loginAttempts: 0,
|
||||
createdAt: '2021-04-21T12:09:55.923Z',
|
||||
rootRole: 1,
|
||||
inviteLink: '',
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
name: 'test',
|
||||
email: 'test@test.no',
|
||||
permissions: [],
|
||||
imageUrl:
|
||||
'https://gravatar.com/avatar/879fdbb54e4a6cdba456fcb11abe5971?size=42&default=retro',
|
||||
seenAt: null,
|
||||
loginAttempts: 0,
|
||||
createdAt: '2021-04-21T15:54:02.765Z',
|
||||
rootRole: 2,
|
||||
inviteLink: '',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Testesen',
|
||||
email: 'test@test.com',
|
||||
permissions: [],
|
||||
imageUrl:
|
||||
'https://gravatar.com/avatar/6c15d63f08137733ec0828cd0a3a5dc4?size=42&default=retro',
|
||||
seenAt: '2021-04-21T14:34:31.515Z',
|
||||
loginAttempts: 0,
|
||||
createdAt: '2021-04-21T12:33:17.712Z',
|
||||
rootRole: 1,
|
||||
inviteLink: '',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'test',
|
||||
email: 'test@test.io',
|
||||
permissions: [],
|
||||
imageUrl:
|
||||
'https://gravatar.com/avatar/879fdbb54e4a6cdba456fcb11abe5971?size=42&default=retro',
|
||||
seenAt: null,
|
||||
loginAttempts: 0,
|
||||
createdAt: '2021-04-21T15:54:02.765Z',
|
||||
rootRole: 2,
|
||||
inviteLink: '',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Testesen',
|
||||
email: 'test@test.uk',
|
||||
permissions: [],
|
||||
imageUrl:
|
||||
'https://gravatar.com/avatar/6c15d63f08137733ec0828cd0a3a5dc4?size=42&default=retro',
|
||||
seenAt: '2021-04-21T14:34:31.515Z',
|
||||
loginAttempts: 0,
|
||||
createdAt: '2021-04-21T12:33:17.712Z',
|
||||
rootRole: 1,
|
||||
inviteLink: '',
|
||||
},
|
||||
];
|
||||
|
||||
export default loadingData;
|
@ -1,64 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dialogue from '../../../component/common/Dialogue';
|
||||
import UserForm from './user-form';
|
||||
|
||||
function AddUser({ showDialog, closeDialog, addUser, roles }) {
|
||||
const [data, setData] = useState({});
|
||||
const [error, setError] = useState({});
|
||||
|
||||
const submit = async e => {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
await addUser(data);
|
||||
setData({});
|
||||
setError({});
|
||||
closeDialog();
|
||||
} catch (error) {
|
||||
const msg = error.message || 'Could not create user';
|
||||
setError({ general: msg });
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = e => {
|
||||
e.preventDefault();
|
||||
setData({});
|
||||
setError({});
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
onClick={e => {
|
||||
submit(e);
|
||||
}}
|
||||
open={showDialog}
|
||||
onClose={onCancel}
|
||||
primaryButtonText="Add user"
|
||||
secondaryButtonText="Cancel"
|
||||
fullWidth
|
||||
>
|
||||
<UserForm title="Add new user" data={data} setData={setData} roles={roles} submit={submit} error={error} />
|
||||
</Dialogue>
|
||||
);
|
||||
}
|
||||
|
||||
AddUser.propTypes = {
|
||||
showDialog: PropTypes.bool.isRequired,
|
||||
closeDialog: PropTypes.func.isRequired,
|
||||
addUser: PropTypes.func.isRequired,
|
||||
validatePassword: PropTypes.func.isRequired,
|
||||
roles: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default AddUser;
|
@ -1,16 +1,29 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextField, DialogTitle, DialogContent } from '@material-ui/core';
|
||||
import classnames from 'classnames';
|
||||
import { TextField, Typography, Avatar } from '@material-ui/core';
|
||||
import { trim } from '../../../component/common/util';
|
||||
import { modalStyles } from './util';
|
||||
import Dialogue from '../../../component/common/Dialogue/Dialogue';
|
||||
import commonStyles from '../../../component/common/common.module.scss';
|
||||
import PasswordChecker from '../../../component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import PasswordMatcher from '../../../component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
|
||||
import ConditionallyRender from '../../../component/common/ConditionallyRender';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
|
||||
function ChangePassword({ showDialog, closeDialog, changePassword, validatePassword, user = {} }) {
|
||||
function ChangePassword({
|
||||
showDialog,
|
||||
closeDialog,
|
||||
changePassword,
|
||||
user = {},
|
||||
}) {
|
||||
const [data, setData] = useState({});
|
||||
const [error, setError] = useState({});
|
||||
const [validPassword, setValidPassword] = useState(false);
|
||||
const commonStyles = useCommonStyles();
|
||||
|
||||
const updateField = e => {
|
||||
setError({});
|
||||
setData({
|
||||
...data,
|
||||
[e.target.name]: trim(e.target.value),
|
||||
@ -19,13 +32,19 @@ function ChangePassword({ showDialog, closeDialog, changePassword, validatePassw
|
||||
|
||||
const submit = async e => {
|
||||
e.preventDefault();
|
||||
if (!data.password || data.password.length < 8) {
|
||||
setError({ password: 'You must specify a password with at least 8 chars.' });
|
||||
return;
|
||||
}
|
||||
if (!(data.password === data.confirm)) {
|
||||
setError({ confirm: 'Passwords does not match' });
|
||||
return;
|
||||
|
||||
if (!validPassword) {
|
||||
if (!data.password || data.password.length < 8) {
|
||||
setError({
|
||||
password:
|
||||
'You must specify a password with at least 8 chars.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!(data.password === data.confirm)) {
|
||||
setError({ confirm: 'Passwords does not match' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -38,19 +57,6 @@ function ChangePassword({ showDialog, closeDialog, changePassword, validatePassw
|
||||
}
|
||||
};
|
||||
|
||||
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 => {
|
||||
e.preventDefault();
|
||||
setData({});
|
||||
@ -64,40 +70,70 @@ function ChangePassword({ showDialog, closeDialog, changePassword, validatePassw
|
||||
style={modalStyles}
|
||||
onClose={onCancel}
|
||||
primaryButtonText="Save"
|
||||
title="Update password"
|
||||
secondaryButtonText="Cancel"
|
||||
>
|
||||
<form onSubmit={submit}>
|
||||
<DialogTitle>Update password</DialogTitle>
|
||||
<DialogContent
|
||||
className={commonStyles.contentSpacing}
|
||||
style={{ display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<p>User: {user.username || user.email}</p>
|
||||
<p style={{ color: 'red' }}>{error.general}</p>
|
||||
<TextField
|
||||
label="New password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={data.password}
|
||||
error={error.password !== undefined}
|
||||
helperText={error.password}
|
||||
onChange={updateField}
|
||||
onBlur={onPasswordBlur}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className={classnames(
|
||||
commonStyles.contentSpacingY,
|
||||
commonStyles.flexColumn
|
||||
)}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={error.general}
|
||||
show={<Alert severity="error">{error.general}</Alert>}
|
||||
/>
|
||||
<Typography variant="subtitle1">
|
||||
Changing password for user
|
||||
</Typography>
|
||||
<div className={commonStyles.flexRow}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
alt={user.name}
|
||||
src={user.imageUrl}
|
||||
title={`${
|
||||
user.name || user.email || user.username
|
||||
} (id: ${user.id})`}
|
||||
/>
|
||||
<TextField
|
||||
label="Confirm password"
|
||||
name="confirm"
|
||||
type="password"
|
||||
value={data.confirm}
|
||||
error={error.confirm !== undefined}
|
||||
helperText={error.confirm}
|
||||
onChange={updateField}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</DialogContent>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
style={{ marginLeft: '1rem' }}
|
||||
>
|
||||
{user.username || user.email}
|
||||
</Typography>
|
||||
</div>
|
||||
<PasswordChecker
|
||||
password={data.password}
|
||||
callback={setValidPassword}
|
||||
/>
|
||||
|
||||
<p style={{ color: 'red' }}>{error.general}</p>
|
||||
<TextField
|
||||
label="New password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={data.password}
|
||||
helperText={error.password}
|
||||
onChange={updateField}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<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}
|
||||
matchingPasswords={data.password === data.confirm}
|
||||
/>
|
||||
</form>
|
||||
</Dialogue>
|
||||
);
|
||||
|
@ -2,27 +2,78 @@ import React from 'react';
|
||||
import Dialogue from '../../../component/common/Dialogue/Dialogue';
|
||||
import ConditionallyRender from '../../../component/common/ConditionallyRender/ConditionallyRender';
|
||||
import propTypes from 'prop-types';
|
||||
import { REMOVE_USER_ERROR } from '../../../hooks/useAdminUsersApi';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import useLoading from '../../../hooks/useLoading';
|
||||
import { Avatar, Typography } from '@material-ui/core';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
|
||||
const DelUserComponent = ({ showDialog, closeDialog, user, removeUser }) => (
|
||||
<ConditionallyRender
|
||||
condition={user}
|
||||
show={
|
||||
<Dialogue
|
||||
open={showDialog}
|
||||
title="Really delete user?"
|
||||
onClose={closeDialog}
|
||||
onClick={() => removeUser(user)}
|
||||
primaryButtonText="Delete user"
|
||||
secondaryButtonText="Cancel"
|
||||
>
|
||||
<div>
|
||||
Are you sure you want to delete{' '}
|
||||
{user ? `${user.name || 'user'} (${user.email || user.username})` : ''}?
|
||||
const DelUserComponent = ({
|
||||
showDialog,
|
||||
closeDialog,
|
||||
user,
|
||||
userLoading,
|
||||
removeUser,
|
||||
userApiErrors,
|
||||
}) => {
|
||||
const ref = useLoading(userLoading);
|
||||
const commonStyles = useCommonStyles();
|
||||
|
||||
return (
|
||||
<Dialogue
|
||||
open={showDialog}
|
||||
title="Really delete user?"
|
||||
onClose={closeDialog}
|
||||
onClick={() => removeUser(user)}
|
||||
primaryButtonText="Delete user"
|
||||
secondaryButtonText="Cancel"
|
||||
>
|
||||
<div ref={ref}>
|
||||
<ConditionallyRender
|
||||
condition={userApiErrors[REMOVE_USER_ERROR]}
|
||||
show={
|
||||
<Alert
|
||||
data-loading
|
||||
severity="error"
|
||||
style={{ margin: '1rem 0' }}
|
||||
>
|
||||
{userApiErrors[REMOVE_USER_ERROR]}
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<div data-loading className={commonStyles.flexRow}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
alt={user.name}
|
||||
src={user.imageUrl}
|
||||
title={`${
|
||||
user.name || user.email || user.username
|
||||
} (id: ${user.id})`}
|
||||
/>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
style={{ marginLeft: '1rem' }}
|
||||
>
|
||||
{user.username || user.email}
|
||||
</Typography>
|
||||
</div>
|
||||
</Dialogue>
|
||||
}
|
||||
/>
|
||||
);
|
||||
<Typography
|
||||
data-loading
|
||||
variant="body1"
|
||||
style={{ marginTop: '1rem' }}
|
||||
>
|
||||
Are you sure you want to delete{' '}
|
||||
{user
|
||||
? `${user.name || 'user'} (${
|
||||
user.email || user.username
|
||||
})`
|
||||
: ''}
|
||||
?
|
||||
</Typography>
|
||||
</div>
|
||||
</Dialogue>
|
||||
);
|
||||
};
|
||||
|
||||
DelUserComponent.propTypes = {
|
||||
showDialog: propTypes.bool.isRequired,
|
||||
|
@ -1,22 +1,25 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dialogue from '../../../component/common/Dialogue';
|
||||
import UserForm from './user-form';
|
||||
import UserForm from './AddUser/AddUserForm/AddUserForm';
|
||||
|
||||
function AddUser({ user = {}, showDialog, closeDialog, updateUser, roles }) {
|
||||
function AddUser({
|
||||
user,
|
||||
showDialog,
|
||||
closeDialog,
|
||||
updateUser,
|
||||
roles,
|
||||
userApiErrors,
|
||||
userLoading,
|
||||
}) {
|
||||
const [data, setData] = useState({});
|
||||
const [error, setError] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setData({
|
||||
id: user.id,
|
||||
email: user.email || '',
|
||||
rootRole: user.rootRole || '',
|
||||
name: user.name || '',
|
||||
...user,
|
||||
});
|
||||
}, [user])
|
||||
|
||||
|
||||
}, [user]);
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
@ -29,7 +32,6 @@ function AddUser({ user = {}, showDialog, closeDialog, updateUser, roles }) {
|
||||
await updateUser(data);
|
||||
setData({});
|
||||
setError({});
|
||||
closeDialog();
|
||||
} catch (error) {
|
||||
setError({ general: 'Could not update user' });
|
||||
}
|
||||
@ -51,9 +53,18 @@ function AddUser({ user = {}, showDialog, closeDialog, updateUser, roles }) {
|
||||
onClose={onCancel}
|
||||
primaryButtonText="Update user"
|
||||
secondaryButtonText="Cancel"
|
||||
title="Update team member"
|
||||
fullWidth
|
||||
>
|
||||
<UserForm title="Update user" data={data} setData={setData} roles={roles} submit={submit} error={error} />
|
||||
<UserForm
|
||||
data={data}
|
||||
setData={setData}
|
||||
roles={roles}
|
||||
submit={submit}
|
||||
error={error}
|
||||
userApiErrors={userApiErrors}
|
||||
userLoading={userLoading}
|
||||
/>
|
||||
</Dialogue>
|
||||
);
|
||||
}
|
||||
|
@ -1,103 +0,0 @@
|
||||
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;
|
@ -1,64 +0,0 @@
|
||||
import api from './api';
|
||||
import { dispatchError } from '../util';
|
||||
export const START_FETCH_USERS = 'START_FETCH_USERS';
|
||||
export const RECEIVE_USERS = 'RECEIVE_USERS';
|
||||
export const ERROR_FETCH_USERS = 'ERROR_FETCH_USERS';
|
||||
export const REMOVE_USER = 'REMOVE_USER';
|
||||
export const REMOVE_USER_ERROR = 'REMOVE_USER_ERROR';
|
||||
export const ADD_USER = 'ADD_USER';
|
||||
export const ADD_USER_ERROR = 'ADD_USER_ERROR';
|
||||
export const UPDATE_USER = 'UPDATE_USER';
|
||||
export const UPDATE_USER_ERROR = 'UPDATE_USER_ERROR';
|
||||
export const CHANGE_PASSWORD_ERROR = 'CHANGE_PASSWORD_ERROR';
|
||||
export const VALIDATE_PASSWORD_ERROR = 'VALIDATE_PASSWORD_ERROR';
|
||||
|
||||
const debug = require('debug')('unleash:e-user-admin-actions');
|
||||
|
||||
const gotUsers = value => ({
|
||||
type: RECEIVE_USERS,
|
||||
value,
|
||||
});
|
||||
|
||||
export function fetchUsers() {
|
||||
debug('Start fetching user');
|
||||
return dispatch => {
|
||||
dispatch({ type: START_FETCH_USERS });
|
||||
|
||||
return api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(gotUsers(json)))
|
||||
.catch(dispatchError(dispatch, ERROR_FETCH_USERS));
|
||||
};
|
||||
}
|
||||
|
||||
export function removeUser(user) {
|
||||
return dispatch =>
|
||||
api
|
||||
.remove(user)
|
||||
.then(() => dispatch({ type: REMOVE_USER, user }))
|
||||
.catch(dispatchError(dispatch, REMOVE_USER_ERROR));
|
||||
}
|
||||
|
||||
export function addUser(user) {
|
||||
return dispatch =>
|
||||
api
|
||||
.create(user)
|
||||
.then(newUser => dispatch({ type: ADD_USER, user: newUser }))
|
||||
.catch(dispatchError(dispatch, ADD_USER_ERROR));
|
||||
}
|
||||
|
||||
export function updateUser(user) {
|
||||
return dispatch =>
|
||||
api
|
||||
.update(user)
|
||||
.then(newUser => dispatch({ type: UPDATE_USER, user: newUser }))
|
||||
.catch(dispatchError(dispatch, UPDATE_USER_ERROR));
|
||||
}
|
||||
|
||||
export function changePassword(user, newPassword) {
|
||||
return dispatch => api.changePassword(user, newPassword).catch(dispatchError(dispatch, CHANGE_PASSWORD_ERROR));
|
||||
}
|
||||
|
||||
export function validatePassword(password) {
|
||||
return dispatch => api.validatePassword(password).catch(dispatchError(dispatch, VALIDATE_PASSWORD_ERROR));
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import { throwIfNotSuccess, headers } from '../api-helper';
|
||||
|
||||
const URI = 'api/admin/user-admin';
|
||||
|
||||
function fetchAll() {
|
||||
return fetch(URI, { headers, credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function create(user) {
|
||||
return fetch(URI, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(user),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function update(user) {
|
||||
return fetch(`${URI}/${user.id}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(user),
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function changePassword(user, newPassword) {
|
||||
return fetch(`${URI}/${user.id}/change-password`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ password: newPassword }),
|
||||
credentials: 'include',
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function validatePassword(password) {
|
||||
return fetch(`${URI}/validate-password`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ password }),
|
||||
credentials: 'include',
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function remove(user) {
|
||||
return fetch(`${URI}/${user.id}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchAll,
|
||||
create,
|
||||
update,
|
||||
changePassword,
|
||||
validatePassword,
|
||||
remove,
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import { List } from 'immutable';
|
||||
import { RECEIVE_USERS, ADD_USER, REMOVE_USER, UPDATE_USER } from './actions';
|
||||
|
||||
const store = (state = new List(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_USERS:
|
||||
return new List(action.value.users);
|
||||
case ADD_USER:
|
||||
return state.push(action.user);
|
||||
case UPDATE_USER:
|
||||
return state.map(user => {
|
||||
if (user.id === action.user.id) {
|
||||
return action.user;
|
||||
} else {
|
||||
return user;
|
||||
}
|
||||
});
|
||||
case REMOVE_USER:
|
||||
return state.filter(v => v.id !== action.user.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default store;
|
@ -1,19 +0,0 @@
|
||||
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;
|
@ -6,7 +6,7 @@ import featureTags from './feature-tags';
|
||||
import tagTypes from './tag-type';
|
||||
import tags from './tag';
|
||||
import strategies from './strategy';
|
||||
import history from "./history"; // eslint-disable-line
|
||||
import history from './history'; // eslint-disable-line
|
||||
import archive from './archive';
|
||||
import error from './error';
|
||||
import settings from './settings';
|
||||
@ -16,8 +16,6 @@ import uiConfig from './ui-config';
|
||||
import context from './context';
|
||||
import projects from './project';
|
||||
import addons from './addons';
|
||||
import userAdmin from './e-user-admin';
|
||||
import roles from './e-user-admin/roles-store';
|
||||
import apiAdmin from './e-api-admin';
|
||||
import authAdmin from './e-admin-auth';
|
||||
import apiCalls from './api-calls';
|
||||
@ -40,8 +38,6 @@ const unleashStore = combineReducers({
|
||||
context,
|
||||
projects,
|
||||
addons,
|
||||
userAdmin,
|
||||
roles,
|
||||
apiAdmin,
|
||||
authAdmin,
|
||||
apiCalls,
|
||||
|
@ -73,6 +73,11 @@ const theme = createMuiTheme({
|
||||
},
|
||||
main: '#fff',
|
||||
},
|
||||
dialogue: {
|
||||
title: {
|
||||
main: '#fff',
|
||||
},
|
||||
},
|
||||
},
|
||||
padding: {
|
||||
pageContent: {
|
||||
|
Loading…
Reference in New Issue
Block a user