From 0ca753e7e50095b29b47a4b3378f358958c50b2c Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Fri, 23 Apr 2021 10:59:11 +0200 Subject: [PATCH] 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 --- frontend/CHANGELOG.md | 2 +- frontend/src/common.styles.js | 8 +- frontend/src/component/App.tsx | 4 +- .../application-edit-component-test.js.snap | 8 +- .../component/common/Dialogue/Dialogue.jsx | 73 +++++-- .../common/Dialogue/Dialogue.styles.js | 14 ++ .../update-variant-component-test.jsx.snap | 4 +- .../view-component-test.jsx.snap | 12 +- .../PasswordChecker/PasswordChecker.styles.ts | 1 + frontend/src/constants/statusCodes.ts | 2 + frontend/src/hooks/useAdminUsersApi.ts | 167 +++++++++++++++ frontend/src/hooks/useLoading.ts | 4 +- frontend/src/hooks/useUsers.ts | 30 +++ frontend/src/icons/email.svg | 17 ++ frontend/src/interfaces/role.ts | 9 + frontend/src/interfaces/user.ts | 28 ++- .../src/page/admin/users/AddUser/AddUser.tsx | 88 ++++++++ .../users/AddUser/AddUserForm/AddUserForm.jsx | 175 ++++++++++++++++ .../AddUser/AddUserForm/AddUserForm.styles.js | 21 ++ .../ConfirmUserAdded/ConfirmUserAdded.tsx | 30 +++ .../ConfirmUserEmail.styles.ts | 11 + .../ConfirmUserEmail/ConfirmUserEmail.tsx | 33 +++ .../ConfirmUserLink/ConfirmUserLink.tsx | 58 ++++++ .../UserInviteLink/UserInviteLink.tsx | 73 +++++++ .../UsersList/UserListItem/UserListItem.tsx | 108 ++++++++++ .../page/admin/users/UsersList/UsersList.jsx | 193 ++++++++++++------ .../src/page/admin/users/UsersList/index.js | 19 +- .../page/admin/users/UsersList/loadingData.ts | 72 +++++++ .../page/admin/users/add-user-component.jsx | 64 ------ .../admin/users/change-password-component.jsx | 146 ++++++++----- .../page/admin/users/del-user-component.jsx | 89 ++++++-- .../admin/users/update-user-component.jsx | 35 ++-- frontend/src/page/admin/users/user-form.jsx | 103 ---------- frontend/src/store/e-user-admin/actions.js | 64 ------ frontend/src/store/e-user-admin/api.js | 66 ------ frontend/src/store/e-user-admin/index.js | 25 --- .../src/store/e-user-admin/roles-store.js | 19 -- frontend/src/store/index.js | 6 +- frontend/src/themes/main-theme.js | 5 + 39 files changed, 1333 insertions(+), 553 deletions(-) create mode 100644 frontend/src/component/common/Dialogue/Dialogue.styles.js create mode 100644 frontend/src/hooks/useAdminUsersApi.ts create mode 100644 frontend/src/hooks/useUsers.ts create mode 100644 frontend/src/icons/email.svg create mode 100644 frontend/src/interfaces/role.ts create mode 100644 frontend/src/page/admin/users/AddUser/AddUser.tsx create mode 100644 frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.jsx create mode 100644 frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.styles.js create mode 100644 frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserAdded.tsx create mode 100644 frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.styles.ts create mode 100644 frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx create mode 100644 frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/ConfirmUserLink.tsx create mode 100644 frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/UserInviteLink/UserInviteLink.tsx create mode 100644 frontend/src/page/admin/users/UsersList/UserListItem/UserListItem.tsx create mode 100644 frontend/src/page/admin/users/UsersList/loadingData.ts delete mode 100644 frontend/src/page/admin/users/add-user-component.jsx delete mode 100644 frontend/src/page/admin/users/user-form.jsx delete mode 100644 frontend/src/store/e-user-admin/actions.js delete mode 100644 frontend/src/store/e-user-admin/api.js delete mode 100644 frontend/src/store/e-user-admin/index.js delete mode 100644 frontend/src/store/e-user-admin/roles-store.js diff --git a/frontend/CHANGELOG.md b/frontend/CHANGELOG.md index 23fd0d0ddd..24109859ef 100644 --- a/frontend/CHANGELOG.md +++ b/frontend/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/frontend/src/common.styles.js b/frontend/src/common.styles.js index cd3bcf0b82..f3d94e7ffc 100644 --- a/frontend/src/common.styles.js +++ b/frontend/src/common.styles.js @@ -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', diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index 45156449a4..62627cc4ba 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -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) => { diff --git a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap index 623d1f4e8b..dd52e7af88 100644 --- a/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap +++ b/frontend/src/component/application/__tests__/__snapshots__/application-edit-component-test.js.snap @@ -144,7 +144,7 @@ exports[`renders correctly with permissions 1`] = `
@@ -272,7 +272,7 @@ exports[`renders correctly with permissions 1`] = ` > ( - - {title} - {children}} /> +}) => { + const styles = useStyles(); + return ( + + {title} + + {children} + + } + /> - - - - - -); + + + {primaryButtonText || "Yes, I'm sure"} + + } + /> + + + {secondaryButtonText || 'No take me back.'}{' '} + + } + /> + + + ); +}; ConfirmDialogue.propTypes = { primaryButtonText: PropTypes.string, diff --git a/frontend/src/component/common/Dialogue/Dialogue.styles.js b/frontend/src/component/common/Dialogue/Dialogue.styles.js new file mode 100644 index 0000000000..42917ea8f2 --- /dev/null +++ b/frontend/src/component/common/Dialogue/Dialogue.styles.js @@ -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', + }, +})); diff --git a/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap b/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap index 7a34a7a3de..e7c6aa4477 100644 --- a/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap +++ b/frontend/src/component/feature/variant/__tests__/__snapshots__/update-variant-component-test.jsx.snap @@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `
Stickiness diff --git a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap index 5e287660dc..e0bc854072 100644 --- a/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap +++ b/frontend/src/component/feature/view/__tests__/__snapshots__/view-component-test.jsx.snap @@ -140,10 +140,10 @@ exports[`renders correctly with one feature 1`] = `
Project @@ -175,7 +175,7 @@ exports[`renders correctly with one feature 1`] = ` >
diff --git a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts index 736bbd3290..e34ad96791 100644 --- a/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts +++ b/frontend/src/component/user/common/ResetPasswordForm/PasswordChecker/PasswordChecker.styles.ts @@ -6,6 +6,7 @@ export const useStyles = makeStyles(theme => ({ borderRadius: '3px', right: '100px', color: '#44606e', + maxWidth: '350px', }, headerContainer: { display: 'flex', padding: '0.5rem' }, divider: { diff --git a/frontend/src/constants/statusCodes.ts b/frontend/src/constants/statusCodes.ts index aeb843466f..a36b13e062 100644 --- a/frontend/src/constants/statusCodes.ts +++ b/frontend/src/constants/statusCodes.ts @@ -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; diff --git a/frontend/src/hooks/useAdminUsersApi.ts b/frontend/src/hooks/useAdminUsersApi.ts new file mode 100644 index 0000000000..cf181f2d23 --- /dev/null +++ b/frontend/src/hooks/useAdminUsersApi.ts @@ -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 => { + 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; diff --git a/frontend/src/hooks/useLoading.ts b/frontend/src/hooks/useLoading.ts index ba76b95753..cea75f342b 100644 --- a/frontend/src/hooks/useLoading.ts +++ b/frontend/src/hooks/useLoading.ts @@ -1,10 +1,10 @@ -import { useEffect, createRef } from 'react'; +import { createRef, useLayoutEffect } from 'react'; type refElement = HTMLDivElement; const useLoading = (loading: boolean) => { const ref = createRef(); - useEffect(() => { + useLayoutEffect(() => { if (ref.current) { const elements = ref.current.querySelectorAll('[data-loading]'); diff --git a/frontend/src/hooks/useUsers.ts b/frontend/src/hooks/useUsers.ts new file mode 100644 index 0000000000..5196858401 --- /dev/null +++ b/frontend/src/hooks/useUsers.ts @@ -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; diff --git a/frontend/src/icons/email.svg b/frontend/src/icons/email.svg new file mode 100644 index 0000000000..ec1e08b84d --- /dev/null +++ b/frontend/src/icons/email.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/src/interfaces/role.ts b/frontend/src/interfaces/role.ts new file mode 100644 index 0000000000..b53a25bd89 --- /dev/null +++ b/frontend/src/interfaces/role.ts @@ -0,0 +1,9 @@ +interface IRole { + id: number; + name: string; + project: string | null; + description: string; + type: string; +} + +export default IRole; diff --git a/frontend/src/interfaces/user.ts b/frontend/src/interfaces/user.ts index 95b6647aee..a707c5fa41 100644 --- a/frontend/src/interfaces/user.ts +++ b/frontend/src/interfaces/user.ts @@ -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; diff --git a/frontend/src/page/admin/users/AddUser/AddUser.tsx b/frontend/src/page/admin/users/AddUser/AddUser.tsx new file mode 100644 index 0000000000..3657cae56b --- /dev/null +++ b/frontend/src/page/admin/users/AddUser/AddUser.tsx @@ -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(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 ( + { + submit(e); + }} + open={showDialog} + onClose={onCancel} + primaryButtonText="Add user" + secondaryButtonText="Cancel" + title="Add team member" + fullWidth + > + + + ); +}; + +export default AddUser; diff --git a/frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.jsx b/frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.jsx new file mode 100644 index 0000000000..1060a12425 --- /dev/null +++ b/frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.jsx @@ -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 ( +
+
+ + + {apiError} + + } + /> +
+ + Who is your team member? + + + {error.general} +

+ } + /> + + + +
+
+
+ + + What is your team member allowed to do? + + + {roles.map(role => ( + + {role.name} + + {role.description} + +
+ } + control={ + + } + value={role.id} + /> + ))} + + + + +
+ ); +} + +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; diff --git a/frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.styles.js b/frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.styles.js new file mode 100644 index 0000000000..5c0893ff09 --- /dev/null +++ b/frontend/src/page/admin/users/AddUser/AddUserForm/AddUserForm.styles.js @@ -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', + }, +})); diff --git a/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserAdded.tsx b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserAdded.tsx new file mode 100644 index 0000000000..b1099e3c26 --- /dev/null +++ b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserAdded.tsx @@ -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 ; + } + + return ( + + ); +}; + +export default ConfirmUserAdded; diff --git a/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.styles.ts b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.styles.ts new file mode 100644 index 0000000000..f8448ba756 --- /dev/null +++ b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.styles.ts @@ -0,0 +1,11 @@ +import { makeStyles } from '@material-ui/styles'; + +export const useStyles = makeStyles({ + iconContainer: { + width: '100%', + textAlign: 'center', + }, + emailIcon: { + margin: '2rem auto', + }, +}); diff --git a/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx new file mode 100644 index 0000000000..8deb364370 --- /dev/null +++ b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserEmail/ConfirmUserEmail.tsx @@ -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 ( + + + 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. + +
+ +
+
+ ); +}; + +export default ConfirmUserEmail; diff --git a/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/ConfirmUserLink.tsx b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/ConfirmUserLink.tsx new file mode 100644 index 0000000000..2a63419217 --- /dev/null +++ b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/ConfirmUserLink.tsx @@ -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 ( + +
+ + A new team member has been added. Please provide them with + the following link to get started: + + + + + 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. + + + + Want to avoid this step in the future?{' '} + {/* TODO - ADD LINK HERE ONCE IT EXISTS*/} + + If you configure an email server for Unleash + {' '} + we'll automatically send informational getting started + emails to new users once you add them. + + +
+
+ ); +}; + +export default ConfirmUserLink; diff --git a/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/UserInviteLink/UserInviteLink.tsx b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/UserInviteLink/UserInviteLink.tsx new file mode 100644 index 0000000000..2999d6f068 --- /dev/null +++ b/frontend/src/page/admin/users/ConfirmUserAdded/ConfirmUserLink/UserInviteLink/UserInviteLink.tsx @@ -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({ + 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 ( +
+ {inviteLink} + + + + + setSnackbar({ show: false, type: 'success', text: '' }) + } + > + {snackbar.text} + +
+ ); +}; + +export default UserInviteLink; diff --git a/frontend/src/page/admin/users/UsersList/UserListItem/UserListItem.tsx b/frontend/src/page/admin/users/UsersList/UserListItem/UserListItem.tsx new file mode 100644 index 0000000000..46085a30dc --- /dev/null +++ b/frontend/src/page/admin/users/UsersList/UserListItem/UserListItem.tsx @@ -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 ( + + + + + + + {formatDateWithLocale(user.createdAt, location.locale)} + + + + + {user.name} + + + + + {user.username || user.email} + + + + + {renderRole(user.rootRole)} + + + + + edit + + + lock + + + delete + + + } + elseShow={} + /> + + ); +}; + +export default UserListItem; diff --git a/frontend/src/page/admin/users/UsersList/UsersList.jsx b/frontend/src/page/admin/users/UsersList/UsersList.jsx index 4c8fa1a605..21dcb8bdf9 100644 --- a/frontend/src/page/admin/users/UsersList/UsersList.jsx +++ b/frontend/src/page/admin/users/UsersList/UsersList.jsx @@ -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 => ( + + )); + } + + return users.map(user => { + return ( + + ); + }); + }; + + if (!users) return null; return ( -
+
@@ -85,63 +169,55 @@ function UsersList({ Name Username Role - {hasAccess('ADMIN') ? 'Action' : ''} + + {hasAccess(ADMIN) ? 'Action' : ''} + - - {users.map(item => ( - - - {formatDateWithLocale(item.createdAt, location.locale)} - {item.name} - {item.username || item.email} - {renderRole(item.rootRole)} - - - edit - - - lock - - - delete - - - } - elseShow={} - /> - - ))} - + {renderUsers()}

+ } elseShow={PS! Only admins can add/remove users.} /> + + - {updateDialog.open && } + /> + { - removeUser(delUser); - closeDelDialog(); - }} + removeUser={onDeleteUser} + userLoading={userLoading} + userApiErrors={userApiErrors} /> )}
diff --git a/frontend/src/page/admin/users/UsersList/index.js b/frontend/src/page/admin/users/UsersList/index.js index 4b8853c049..d3083cc9fb 100644 --- a/frontend/src/page/admin/users/UsersList/index.js +++ b/frontend/src/page/admin/users/UsersList/index.js @@ -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; diff --git a/frontend/src/page/admin/users/UsersList/loadingData.ts b/frontend/src/page/admin/users/UsersList/loadingData.ts new file mode 100644 index 0000000000..e216574171 --- /dev/null +++ b/frontend/src/page/admin/users/UsersList/loadingData.ts @@ -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; diff --git a/frontend/src/page/admin/users/add-user-component.jsx b/frontend/src/page/admin/users/add-user-component.jsx deleted file mode 100644 index ff05f18634..0000000000 --- a/frontend/src/page/admin/users/add-user-component.jsx +++ /dev/null @@ -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 ( - { - submit(e); - }} - open={showDialog} - onClose={onCancel} - primaryButtonText="Add user" - secondaryButtonText="Cancel" - fullWidth - > - - - ); -} - -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; diff --git a/frontend/src/page/admin/users/change-password-component.jsx b/frontend/src/page/admin/users/change-password-component.jsx index 6a305c5579..8c97fe4c4d 100644 --- a/frontend/src/page/admin/users/change-password-component.jsx +++ b/frontend/src/page/admin/users/change-password-component.jsx @@ -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" > -
- Update password - -

User: {user.username || user.email}

-

{error.general}

- + {error.general}} + /> + + Changing password for user + +
+ - - + + {user.username || user.email} + +
+ + +

{error.general}

+ + + ); diff --git a/frontend/src/page/admin/users/del-user-component.jsx b/frontend/src/page/admin/users/del-user-component.jsx index a409d79c0e..b2ee4ac289 100644 --- a/frontend/src/page/admin/users/del-user-component.jsx +++ b/frontend/src/page/admin/users/del-user-component.jsx @@ -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 }) => ( - removeUser(user)} - primaryButtonText="Delete user" - secondaryButtonText="Cancel" - > -
- 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 ( + removeUser(user)} + primaryButtonText="Delete user" + secondaryButtonText="Cancel" + > +
+ + {userApiErrors[REMOVE_USER_ERROR]} + + } + /> +
+ + + {user.username || user.email} +
- - } - /> -); + + Are you sure you want to delete{' '} + {user + ? `${user.name || 'user'} (${ + user.email || user.username + })` + : ''} + ? + +
+
+ ); +}; DelUserComponent.propTypes = { showDialog: propTypes.bool.isRequired, diff --git a/frontend/src/page/admin/users/update-user-component.jsx b/frontend/src/page/admin/users/update-user-component.jsx index 13e0a8f52d..ef28aa3021 100644 --- a/frontend/src/page/admin/users/update-user-component.jsx +++ b/frontend/src/page/admin/users/update-user-component.jsx @@ -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 > - + ); } diff --git a/frontend/src/page/admin/users/user-form.jsx b/frontend/src/page/admin/users/user-form.jsx deleted file mode 100644 index aa811bf9b1..0000000000 --- a/frontend/src/page/admin/users/user-form.jsx +++ /dev/null @@ -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 ( -
- {title} - - -

{error.general}

- - -
-
- - Role - - {roles.map(role => ( - - {role.name} -

{role.description}

-
- } - control={} - value={role.id} - /> - ))} - - -
- - ); -} - -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; diff --git a/frontend/src/store/e-user-admin/actions.js b/frontend/src/store/e-user-admin/actions.js deleted file mode 100644 index 3131559bdc..0000000000 --- a/frontend/src/store/e-user-admin/actions.js +++ /dev/null @@ -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)); -} diff --git a/frontend/src/store/e-user-admin/api.js b/frontend/src/store/e-user-admin/api.js deleted file mode 100644 index bac580d995..0000000000 --- a/frontend/src/store/e-user-admin/api.js +++ /dev/null @@ -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, -}; diff --git a/frontend/src/store/e-user-admin/index.js b/frontend/src/store/e-user-admin/index.js deleted file mode 100644 index 165ccc1b6d..0000000000 --- a/frontend/src/store/e-user-admin/index.js +++ /dev/null @@ -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; diff --git a/frontend/src/store/e-user-admin/roles-store.js b/frontend/src/store/e-user-admin/roles-store.js deleted file mode 100644 index 7e3e71b11b..0000000000 --- a/frontend/src/store/e-user-admin/roles-store.js +++ /dev/null @@ -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; diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index a565205b75..d3726c52a5 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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, diff --git a/frontend/src/themes/main-theme.js b/frontend/src/themes/main-theme.js index 756b660817..f822d5842a 100644 --- a/frontend/src/themes/main-theme.js +++ b/frontend/src/themes/main-theme.js @@ -73,6 +73,11 @@ const theme = createMuiTheme({ }, main: '#fff', }, + dialogue: { + title: { + main: '#fff', + }, + }, }, padding: { pageContent: {