diff --git a/frontend/src/components/OrganizationList/OrganizationList.tsx b/frontend/src/components/OrganizationList/OrganizationList.tsx index 8321f388..eb1b4601 100644 --- a/frontend/src/components/OrganizationList/OrganizationList.tsx +++ b/frontend/src/components/OrganizationList/OrganizationList.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import EditNoteOutlinedIcon from '@mui/icons-material/EditNoteOutlined'; import { Organization } from 'types'; -import { Alert, Box, Button, IconButton, Grid } from '@mui/material'; +import { Alert, Box, Button, IconButton, Paper } from '@mui/material'; import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { useHistory } from 'react-router-dom'; import { Add } from '@mui/icons-material'; @@ -103,12 +103,7 @@ export const OrganizationList: React.FC<{ return ( - - + {organizations?.length === 0 ? ( No organizations found. ) : ( @@ -121,7 +116,7 @@ export const OrganizationList: React.FC<{ }} /> )} - + { - global?: string; -} +type ErrorStates = { + getUsersError: string; + getAddUserError: string; + getDeleteError: string; +}; export interface ApiResponse { result: User[]; @@ -34,194 +38,212 @@ export interface ApiResponse { url?: string; } +interface UserType extends User { + lastLoggedInString?: string | null | undefined; + dateToUSigned?: string | null | undefined; + orgs?: string | null | undefined; +} + +type UserFormValues = { + firstName: string; + lastName: string; + email: string; + organization?: Organization; + userType: string; +}; + +const initialUserFormValues = { + firstName: '', + lastName: '', + email: '', + userType: '' +}; + +type CloseReason = 'backdropClick' | 'escapeKeyDown' | 'closeButtonClick'; + export const Users: React.FC = () => { - const { user, apiPost, apiDelete } = useAuthContext(); - const modalRef = useRef(null); - const [selectedRow, setSelectedRow] = useState(0); - const [users, setUsers] = useState([]); + const { user, apiGet, apiPost, apiDelete } = useAuthContext(); + const [selectedRow, setSelectedRow] = useState(initializeUser); + const [users, setUsers] = useState([]); + const [newUserDialogOpen, setNewUserDialogOpen] = useState(false); + const [deleteUserDialogOpen, setDeleteUserDialogOpen] = useState(false); + const [infoDialogOpen, setInfoDialogOpen] = useState(false); + const [infoDialogContent, setInfoDialogContent] = useState(''); + const [formErrors, setFormErrors] = useState({ + firstName: false, + lastName: false, + email: false, + userType: false + }); + const [errorStates, setErrorStates] = useState({ + getUsersError: '', + getAddUserError: '', + getDeleteError: '' + }); + const [values, setValues] = useState(initialUserFormValues); - const columns: Column[] = [ - { - Header: 'Name', - accessor: 'fullName', - width: 200, - disableFilters: true, - id: 'name' - }, - { - Header: 'Email', - accessor: 'email', - width: 150, - minWidth: 150, - id: 'email', - disableFilters: true - }, - { - Header: 'Organizations', - accessor: ({ roles }) => - roles && - roles - .filter((role) => role.approved) - .map((role) => role.organization.name) - .join(', '), - id: 'organizations', - width: 200, - disableFilters: true, - disableSortBy: true - }, + const fetchUsers = useCallback(async () => { + try { + const rows = await apiGet(`/users/`); + rows.forEach((obj) => { + obj.lastLoggedInString = obj.lastLoggedIn + ? `${formatDistanceToNow(parseISO(obj.lastLoggedIn))} ago` + : 'None'; + obj.dateToUSigned = obj.dateAcceptedTerms + ? `${formatDistanceToNow(parseISO(obj.dateAcceptedTerms))} ago` + : 'None'; + obj.orgs = obj.roles + ? obj.roles + .filter((role) => role.approved) + .map((role) => role.organization.name) + .join(', ') + : 'None'; + }); + setUsers(rows); + setErrorStates({ ...errorStates, getUsersError: '' }); + } catch (e: any) { + setErrorStates({ ...errorStates, getUsersError: e.message }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [apiGet]); + + useEffect(() => { + fetchUsers(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const userCols: GridColDef[] = [ + { field: 'fullName', headerName: 'Name', minWidth: 100, flex: 1 }, + { field: 'email', headerName: 'Email', minWidth: 100, flex: 1.75 }, { - Header: 'User type', - accessor: ({ userType }) => - userType === 'standard' - ? 'Standard' - : userType === 'globalView' - ? 'Global View' - : 'Global Admin', - width: 50, - minWidth: 50, - id: 'userType', - disableFilters: true + field: 'orgs', + headerName: 'Organizations', + minWidth: 100, + flex: 1 }, + { field: 'userType', headerName: 'User Type', minWidth: 100, flex: 0.75 }, { - Header: 'Date ToU Signed', - accessor: ({ dateAcceptedTerms }) => - dateAcceptedTerms - ? `${formatDistanceToNow(parseISO(dateAcceptedTerms))} ago` - : 'None', - width: 50, - minWidth: 50, - id: 'dateAcceptedTerms', - disableFilters: true + field: 'dateToUSigned', + headerName: 'Date ToU Signed', + minWidth: 100, + flex: 0.75 }, { - Header: 'ToU Version', - accessor: 'acceptedTermsVersion', - width: 50, - minWidth: 50, - id: 'acceptedTermsVersion', - disableFilters: true + field: 'acceptedTermsVersion', + headerName: 'ToU Version', + minWidth: 100, + flex: 0.5 }, { - Header: 'Last Logged In', - accessor: ({ lastLoggedIn }) => - lastLoggedIn - ? `${formatDistanceToNow(parseISO(lastLoggedIn))} ago` - : 'None', - width: 50, - minWidth: 50, - id: 'lastLoggedIn', - disableFilters: true + field: 'lastLoggedInString', + headerName: 'Last Logged In', + minWidth: 100, + flex: 0.75 }, { - Header: 'Delete', - id: 'delete', - Cell: ({ row }: { row: { index: number } }) => ( - { - modalRef.current?.toggleModal(undefined, true); - setSelectedRow(row.index); - }} - > - - - ), - disableFilters: true + field: 'delete', + headerName: 'Delete', + minWidth: 100, + flex: 0.4, + renderCell: (cellValues: GridRenderCellParams) => { + return ( + { + setSelectedRow(cellValues.row); + setDeleteUserDialogOpen(true); + }} + > + + + ); + } } ]; - const [errors, setErrors] = useState({}); - const [values, setValues] = useState<{ - firstName: string; - lastName: string; - email: string; - organization?: Organization; - userType: string; - }>({ - firstName: '', - lastName: '', - email: '', - userType: '' - }); - const userSearch = useCallback( - async ({ - sort, - groupBy = undefined - }: { - sort: SortingRule[]; - groupBy?: string; - }): Promise => { - try { - const tableFilters: any = {}; - return await apiPost('/users/search', { - body: { - page: 1, - sort: sort[0]?.id ?? 'email', - order: sort[0]?.desc ? 'DESC' : 'ASC', - filters: tableFilters, - pageSize: -1, - groupBy - } - }); - } catch (e) { - console.error(e); - return; - } - }, - [apiPost] + const addUserButton = user?.userType === 'globalAdmin' && ( + } + onClick={() => setNewUserDialogOpen(true)} + > + Invite New User + ); - const fetchUsers = useCallback( - async (query: Query) => { - const resp = await userSearch({ - sort: query.sort - }); - if (!resp) return; - const { result } = resp; - setUsers(result); - }, - [userSearch] - ); + const handleCloseAddUserDialog = (value: CloseReason) => { + if (value === 'backdropClick' || value === 'escapeKeyDown') { + return; + } + setNewUserDialogOpen(false); + setFormErrors({ + firstName: false, + lastName: false, + email: false, + userType: false + }); + }; - const deleteRow = async (index: number) => { + const deleteRow = async (row: UserType) => { try { - const row = users[index]; await apiDelete(`/users/${row.id}`, { body: {} }); setUsers(users.filter((user) => user.id !== row.id)); + setErrorStates({ ...errorStates, getUsersError: '' }); + setInfoDialogContent('This user has been successfully removed.'); + setDeleteUserDialogOpen(false); + setInfoDialogOpen(true); } catch (e: any) { - setErrors({ - global: - e.status === 422 ? 'Unable to delete user' : e.message ?? e.toString() - }); + setErrorStates({ ...errorStates, getDeleteError: e.message }); + setInfoDialogContent( + 'This user has been not been removed. Check the console log for more details.' + ); console.log(e); } }; - const onSubmit: React.FormEventHandler = async (e) => { + const onSubmit = async (e: any) => { e.preventDefault(); + console.log(e); + const body = { + firstName: values.firstName, + lastName: values.lastName, + email: values.email, + userType: values.userType + }; + const { firstName, lastName, email, userType } = values; + const newFormErrors = { + firstName: !firstName, + lastName: !lastName, + email: !email, + userType: !userType + }; + setFormErrors(newFormErrors); + if (Object.values(newFormErrors).some((error) => error)) { + return; + } try { - const body = { - firstName: values.firstName, - lastName: values.lastName, - email: values.email, - userType: values.userType - }; const user = await apiPost('/users/', { body }); setUsers(users.concat(user)); + setErrorStates({ ...errorStates, getAddUserError: '' }); + handleCloseAddUserDialog('closeButtonClick'); + setInfoDialogContent('This user has been successfully added.'); + setInfoDialogOpen(true); + setValues(initialUserFormValues); } catch (e: any) { - setErrors({ - global: - e.status === 422 - ? 'Error when submitting user entry.' - : e.message ?? e.toString() - }); + setErrorStates({ ...errorStates, getAddUserError: e.message }); + setInfoDialogContent( + 'This user has been not been added. Check the console log for more details.' + ); console.log(e); + setValues(initialUserFormValues); } }; const onTextChange: React.ChangeEventHandler< - HTMLInputElement | HTMLSelectElement + HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement > = (e) => onChange(e.target.name, e.target.value); const onChange = (name: string, value: any) => { @@ -231,72 +253,166 @@ export const Users: React.FC = () => { })); }; + const textFieldStyling = { + '& .MuiOutlinedInput-root': { + '&.Mui-focused fieldset': { + borderRadius: '0px' + } + } + }; + + const confirmDeleteUserDialog = ( + { + deleteRow(selectedRow); + }} + onCancel={() => setDeleteUserDialogOpen(false)} + title={'Are you sure you want to delete this user?'} + content={ + <> + + This request will permanently remove {selectedRow?.fullName}{' '} + from Crossfeed and cannot be undone. + + {errorStates.getDeleteError && ( + + Error removing user: {errorStates.getDeleteError}. See the network + tab for more details. + + )} + + } + screenWidth="xs" + /> + ); + return (

Users

- columns={columns} data={users} fetchData={fetchUsers} /> -

Invite a user

-
- {errors.global &&

{errors.global}

} - - - - - - + - - - } - label="Standard" + + {confirmDeleteUserDialog} + handleCloseAddUserDialog(reason)} + fullWidth + maxWidth="xs" + > + Invite a User + + First Name + - } - label="Global View" + Last Name + - } - label="Global Administrator" + Email + - -

- - + User Type + + } + label="Standard" + /> + } + label="Global View" + /> + } + label="Global Administrator" + /> + + {formErrors.userType && ( + + User Type is required + + )} + {errorStates.getAddUserError && ( + + Error adding user to the database: {errorStates.getAddUserError}. + See the network tab for more details. + + )} + + + { + setNewUserDialogOpen(false); + setFormErrors({ + firstName: false, + lastName: false, + email: false, + userType: false + }); + setValues(initialUserFormValues); + }} + > + Cancel + + + Invite User + + + {user?.userType === 'globalAdmin' && ( <> { /> )} - - - Delete user? -

- Are you sure you would like to delete{' '} - {users[selectedRow]?.fullName}? -

- - - { - deleteRow(selectedRow); - }} - > - Delete - - - Cancel - - - -
+ { + setInfoDialogOpen(false); + window.location.reload(); + }} + icon={} + title={Success } + content={{infoDialogContent}} + />
); };