Merge pull request #139 from stephb9959/main

[WIFI-11866] User role and user edit fixes
This commit is contained in:
Charles Bourque
2022-12-01 16:35:53 +00:00
committed by GitHub
19 changed files with 767 additions and 639 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.8.0(31)",
"version": "2.8.0(32)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.8.0(31)",
"version": "2.8.0(32)",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.11",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.8.0(31)",
"version": "2.8.0(32)",
"description": "",
"private": true,
"main": "index.tsx",

View File

@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { User } from '../../models/User';
import { UserRole } from './Users';
import { axiosSec } from 'constants/axiosInstances';
import { AxiosError } from 'models/Axios';
import { Note } from 'models/Note';
@@ -141,6 +142,7 @@ export const useUpdateAccount = ({ user }: { user?: User }) => {
};
mobiles?: { number: string }[];
};
userRole?: UserRole;
notes?: Note[];
}) => axiosSec.put(`user/${user?.id ?? userInfo?.id}`, userInfo),
{

View File

@@ -1,9 +1,62 @@
import { useToast } from '@chakra-ui/react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { axiosSec } from 'constants/axiosInstances';
import { AxiosError } from 'models/Axios';
import { User } from 'models/User';
import { AtLeast } from 'models/General';
import { Note } from 'models/Note';
export type UserRole =
| 'root'
| 'admin'
| 'subscriber'
| 'partner'
| 'csr'
| 'system'
| 'installer'
| 'noc'
| 'accounting';
export type User = {
avatar: string;
blackListed: boolean;
creationDate: number;
currentLoginURI: string;
currentPassword: string;
description: string;
email: string;
id: string;
lastEmailCheck: number;
lastLogin: number;
lastPasswordChange: number;
lastPasswords: string[];
locale: string;
location: string;
modified: number;
name: string;
notes: Note[];
oauthType: string;
oauthUserInfo: string;
owner: string;
securityPolicy: string;
securityPolicyChange: number;
signingUp: string;
suspended: boolean;
userRole: UserRole;
userTypeProprietaryInfo: {
authenticatorSecret: string;
mfa: {
enabled: boolean;
method?: 'authenticator' | 'sms' | 'email' | '';
};
mobiles: { number: string }[];
};
validated: boolean;
validationDate: number;
validationEmail: string;
validationURI: string;
waitingForEmailCheck: boolean;
};
const getAvatarPromises = (userList: User[]) => {
const promises = userList.map(async (user) => {
@@ -18,12 +71,9 @@ const getAvatarPromises = (userList: User[]) => {
return promises;
};
export const useGetUsers = ({ setUsersWithAvatars }: { setUsersWithAvatars: (users: unknown) => void }) => {
const { t } = useTranslation();
const toast = useToast();
const getUsers = async () => {
const users = await axiosSec.get('users').then(({ data }) => data.users as User[]);
return useQuery(['get-users'], () => axiosSec.get('users').then(({ data }) => data.users), {
onSuccess: async (users) => {
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
results.map((response) => {
if (response.status === 'fulfilled' && response?.value !== '') {
@@ -37,9 +87,14 @@ export const useGetUsers = ({ setUsersWithAvatars }: { setUsersWithAvatars: (use
}),
);
const newUsers = users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] }));
setUsersWithAvatars(newUsers);
},
return users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] })) as User[];
};
export const useGetUsers = () => {
const { t } = useTranslation();
const toast = useToast();
return useQuery(['users'], getUsers, {
onError: (e: AxiosError) => {
if (!toast.isActive('users-fetching-error'))
toast({
@@ -62,7 +117,10 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
const { t } = useTranslation();
const toast = useToast();
return useQuery(['get-user', id], () => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data), {
return useQuery(
['get-user', id],
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
{
enabled,
onError: (e: AxiosError) => {
if (!toast.isActive('user-fetching-error'))
@@ -79,7 +137,8 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
position: 'top-right',
});
},
});
},
);
};
export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refresh: () => void }) => {
@@ -124,3 +183,45 @@ export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSe
export const useResetPassword = ({ id }: { id: string }) =>
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
export const useDeleteUser = () => {
const queryClient = useQueryClient();
return useMutation(deleteUser, {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
});
};
const createUser = async (newUser: {
name: string;
description?: string;
email: string;
currentPassword: string;
notes?: { note: string }[];
userRole: string;
emailValidation: boolean;
changePassword: boolean;
}) => axiosSec.post(`user/0${newUser.emailValidation ? '?email_verification=true' : ''}`, newUser);
export const useCreateUser = () => {
const queryClient = useQueryClient();
return useMutation(createUser, {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
});
};
const modifyUser = async (newUser: AtLeast<User, 'id'>) => axiosSec.put(`user/${newUser.id}`, newUser);
export const useUpdateUser = () => {
const queryClient = useQueryClient();
return useMutation(modifyUser, {
onSuccess: () => {
queryClient.invalidateQueries(['users']);
},
});
};

View File

@@ -2,7 +2,8 @@ import { Ref, useCallback, useMemo, useState } from 'react';
import { FormikProps } from 'formik';
import { FormType } from '../models/Form';
export const useFormRef = () => {
// eslint-disable-next-line import/prefer-default-export
export const useFormRef = <Type = Record<string, unknown>>() => {
const [form, setForm] = useState<FormType>({
submitForm: () => {},
isSubmitting: false,
@@ -10,7 +11,7 @@ export const useFormRef = () => {
dirty: false,
});
const formRef = useCallback(
(node: FormikProps<Record<string, unknown>> | undefined) => {
(node: FormikProps<Type>) => {
if (
node &&
(form.submitForm !== node.submitForm ||
@@ -22,7 +23,7 @@ export const useFormRef = () => {
}
},
[form],
) as Ref<FormikProps<Record<string, unknown>>> | undefined;
) as Ref<FormikProps<Type>>;
const toReturn = useMemo(() => ({ form, formRef }), [form]);

1
src/models/General.ts Normal file
View File

@@ -0,0 +1 @@
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;

View File

@@ -1,5 +1,6 @@
import { Note } from './Note';
export type UserRole =
| 'root'
| 'admin'
@@ -11,13 +12,31 @@ export type UserRole =
| 'noc'
| 'accounting';
export interface User {
name: string;
export type User = {
avatar: string;
blackListed: boolean;
creationDate: number;
currentLoginURI: string;
currentPassword: string;
description: string;
currentPassword?: string;
id: string;
email: string;
id: string;
lastEmailCheck: number;
lastLogin: number;
lastPasswordChange: number;
lastPasswords: string[];
locale: string;
location: string;
modified: number;
name: string;
notes: Note[];
oauthType: string;
oauthUserInfo: string;
owner: string;
securityPolicy: string;
securityPolicyChange: number;
signingUp: string;
suspended: boolean;
userRole: UserRole;
userTypeProprietaryInfo: {
authenticatorSecret: string;
@@ -27,6 +46,9 @@ export interface User {
};
mobiles: { number: string }[];
};
suspended: boolean;
notes: Note[];
}
validated: boolean;
validationDate: number;
validationEmail: string;
validationURI: string;
waitingForEmailCheck: boolean;
};

View File

@@ -0,0 +1,89 @@
import * as React from 'react';
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Button,
Center,
Spinner,
useDisclosure,
} from '@chakra-ui/react';
import axios from 'axios';
import { useTranslation } from 'react-i18next';
import { DeleteButton } from '../../components/Buttons/DeleteButton';
import { Modal } from '../../components/Modals/Modal';
import { useAuth } from 'contexts/AuthProvider';
import { useDeleteUser } from 'hooks/Network/Users';
type Props = {
isDisabled?: boolean;
};
const DeleteProfileButton = ({ isDisabled }: Props) => {
const { t } = useTranslation();
const { user, logout } = useAuth();
const deleteUser = useDeleteUser();
const modalProps = useDisclosure();
const onDeleteClick = () =>
deleteUser.mutate(user?.id ?? '', {
onSuccess: () => {
setTimeout(() => {
logout();
}, 3000);
},
});
const onOpen = () => {
deleteUser.reset();
modalProps.onOpen();
};
return (
<>
<DeleteButton isCompact isDisabled={isDisabled} onClick={onOpen} />
<Modal {...modalProps} title={t('profile.delete_account')}>
<Box>
{deleteUser.isSuccess ? (
<Center>
<Alert status="success">
<AlertIcon />
<AlertDescription>
{t('Your profile is now deleted, we will now log you out...')} <Spinner />
</AlertDescription>
</Alert>
</Center>
) : (
<>
<Center>
{deleteUser.error ? (
<Alert status="error">
<AlertIcon />
<Box>
<AlertTitle>{t('common.error')}</AlertTitle>
<AlertDescription>
{axios.isAxiosError(deleteUser.error) ? deleteUser.error.response?.data?.ErrorDescription : ''}
</AlertDescription>
</Box>
</Alert>
) : (
<Alert status="warning">
<AlertIcon />
<AlertDescription>{t('profile.delete_warning')}</AlertDescription>
</Alert>
)}
</Center>
<Center my={8}>
<Button onClick={onDeleteClick} isLoading={deleteUser.isLoading} colorScheme="red">
{t('profile.delete_account_confirm')}
</Button>
</Center>
</>
)}
</Box>
</Modal>
</>
);
};
export default DeleteProfileButton;

View File

@@ -1,20 +1,24 @@
import * as React from 'react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Box, Center, Flex, Heading, Link, Spacer, Spinner, useToast } from '@chakra-ui/react';
import { Box, Center, Flex, Heading, HStack, Link, Spacer, Spinner, useToast } from '@chakra-ui/react';
import axios from 'axios';
import { Form, Formik, FormikProps } from 'formik';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import * as Yup from 'yup';
import DeleteProfileButton from './DeleteButton';
import { SaveButton } from 'components/Buttons/SaveButton';
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { SelectField } from 'components/Form/Fields/SelectField';
import { StringField } from 'components/Form/Fields/StringField';
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
import { useAuth } from 'contexts/AuthProvider';
import { testRegex } from 'helpers/formTests';
import { useUpdateAccount } from 'hooks/Network/Account';
import { UserRole } from 'hooks/Network/Users';
import { useApiRequirements } from 'hooks/useApiRequirements';
import { useFormModal } from 'hooks/useFormModal';
import { useFormRef } from 'hooks/useFormRef';
@@ -70,13 +74,16 @@ const GeneralInformationProfile = () => {
<CardHeader mb={2}>
<Heading size="md">{t('profile.your_profile')}</Heading>
<Spacer />
<HStack>
<SaveButton
onClick={form.submitForm}
isLoading={form.isSubmitting}
isDisabled={!form.isValid || !form.dirty}
hidden={!isEditing}
/>
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} ml={2} />
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} />
<DeleteProfileButton isDisabled={isEditing} />
</HStack>
</CardHeader>
<CardBody display="block">
{!user ? (
@@ -89,6 +96,7 @@ const GeneralInformationProfile = () => {
firstName: string;
lastName: string;
newPassword?: string;
userRole: UserRole;
}>
key={formKey}
initialValues={
@@ -97,11 +105,13 @@ const GeneralInformationProfile = () => {
description: user?.description ?? '',
firstName: user?.name.split(' ')[0] ?? '',
lastName: user?.name.split(' ')[1] ?? '',
userRole: user?.userRole,
} as {
description: string;
firstName: string;
lastName: string;
newPassword?: string;
userRole: UserRole;
}
}
innerRef={
@@ -111,17 +121,19 @@ const GeneralInformationProfile = () => {
firstName: string;
lastName: string;
newPassword?: string;
userRole: UserRole;
}>
>
}
validationSchema={FormSchema(t, { passRegex: passwordPattern })}
onSubmit={async ({ description, firstName, lastName, newPassword }, { setSubmitting }) => {
onSubmit={async ({ description, firstName, lastName, newPassword, userRole }, { setSubmitting }) => {
await updateUser.mutateAsync(
{
id: user?.id,
description,
name: `${firstName} ${lastName}`,
currentPassword: newPassword,
userRole: user?.userRole === 'root' ? userRole : undefined,
},
{
onSuccess: () => {
@@ -139,13 +151,45 @@ const GeneralInformationProfile = () => {
position: 'top-right',
});
},
onError: (e) => {
if (axios.isAxiosError(e)) {
toast({
id: 'account-update-error',
title: t('common.error'),
description: e.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
}
},
},
);
}}
>
{({ isSubmitting }) => (
<Form>
<Flex>
<StringField name="email" label={t('common.email')} isDisabled />
<Box w={8} />
<SelectField
name="userRole"
label={t('user.role')}
options={[
{ value: 'accounting', label: 'Accounting' },
{ value: 'admin', label: 'Admin' },
{ value: 'csr', label: 'CSR' },
{ value: 'installer', label: 'Installer' },
{ value: 'noc', label: 'NOC' },
{ value: 'root', label: 'Root' },
{ value: 'system', label: 'System' },
]}
isRequired
isDisabled={isSubmitting || !isEditing || user?.userRole !== 'root'}
w="max-content"
/>
</Flex>
<Flex my={4}>
<StringField
name="firstName"

View File

@@ -1,11 +1,11 @@
import React from 'react';
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip, useToast } from '@chakra-ui/react';
import axios from 'axios';
import { Wrench } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { useSendUserEmailValidation, useSuspendUser, useResetMfa, useResetPassword } from 'hooks/Network/Users';
import { useResetMfa, useResetPassword, useSendUserEmailValidation, useSuspendUser } from 'hooks/Network/Users';
import { useMutationResult } from 'hooks/useMutationResult';
import { AxiosError } from 'models/Axios';
interface Props {
id: string;
@@ -32,7 +32,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
onSuccess();
},
onError: (e) => {
if (axios.isAxiosError(e)) onError(e);
onError(e as AxiosError);
},
});
const handleResetMfaClick = () =>
@@ -49,7 +49,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
});
},
onError: (e) => {
if (axios.isAxiosError(e)) onError(e);
onError(e as AxiosError);
},
});
@@ -67,7 +67,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
});
},
onError: (e) => {
if (axios.isAxiosError(e)) onError(e);
onError(e as AxiosError);
},
});

View File

@@ -1,200 +0,0 @@
import React, { useEffect, useState } from 'react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Box, Flex, Link, useToast, SimpleGrid } from '@chakra-ui/react';
import { Formik, Form } from 'formik';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import * as Yup from 'yup';
import { SelectField } from 'components/Form/Fields/SelectField';
import { StringField } from 'components/Form/Fields/StringField';
import { ToggleField } from 'components/Form/Fields/ToggleField';
import { useAuth } from 'contexts/AuthProvider';
import { testRegex } from 'helpers/formTests';
import { useApiRequirements } from 'hooks/useApiRequirements';
const propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
createUser: PropTypes.instanceOf(Object).isRequired,
refreshUsers: PropTypes.func.isRequired,
formRef: PropTypes.instanceOf(Object).isRequired,
};
const CreateUserSchema = (t, { passRegex }) =>
Yup.object().shape({
email: Yup.string().email(t('form.invalid_email')).required('Required'),
name: Yup.string().required('Required'),
description: Yup.string(),
currentPassword: Yup.string()
.required(t('form.required'))
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex))
.default(''),
note: Yup.string(),
userRole: Yup.string(),
});
const CreateUserNonRootSchema = (t, { passRegex }) =>
Yup.object().shape({
email: Yup.string().email(t('form.invalid_email')).required('Required'),
name: Yup.string().required('Required'),
description: Yup.string(),
currentPassword: Yup.string()
.required(t('form.required'))
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex))
.default(''),
note: Yup.string(),
userRole: Yup.string(),
});
const CreateUserForm = ({ isOpen, onClose, createUser, refreshUsers, formRef }) => {
const { t } = useTranslation();
const toast = useToast();
const { user } = useAuth();
const [formKey, setFormKey] = useState(uuid());
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
const createParameters = ({
name,
description,
email,
currentPassword,
note,
userRole,
emailValidation,
changePassword,
}) => {
if (userRole === 'root') {
return {
name,
email,
currentPassword,
userRole,
description: description.length > 0 ? description : undefined,
notes: note.length > 0 ? [{ note }] : undefined,
emailValidation,
changePassword,
};
}
return {
name,
email,
currentPassword,
userRole,
description: description.length > 0 ? description : undefined,
notes: note.length > 0 ? [{ note }] : undefined,
emailValidation,
changePassword,
};
};
useEffect(() => {
setFormKey(uuid());
}, [isOpen]);
return (
<Formik
innerRef={formRef}
key={formKey}
initialValues={{
name: '',
description: '',
email: '',
currentPassword: '',
note: '',
userRole: user.userRole === 'admin' ? 'csr' : user.userRole,
changePassword: true,
emailValidation: true,
}}
validationSchema={
user?.userRole === 'root'
? CreateUserSchema(t, { passRegex: passwordPattern })
: CreateUserNonRootSchema(t, { passRegex: passwordPattern })
}
onSubmit={(formData, { setSubmitting, resetForm }) =>
createUser.mutateAsync(createParameters(formData), {
onSuccess: () => {
setSubmitting(false);
resetForm();
toast({
id: 'user-creation-success',
title: t('common.success'),
description: t('crud.success_create_obj', {
obj: t('user.title'),
}),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
refreshUsers();
onClose();
},
onError: (e) => {
toast({
id: uuid(),
title: t('common.error'),
description: t('crud.error_create_obj', {
obj: t('user.title'),
e: e?.response?.data?.ErrorDescription,
}),
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
},
})
}
>
{({ errors, touched }) => (
<Form>
<SimpleGrid minChildWidth="300px" spacing="20px">
<StringField name="email" label={t('common.email')} errors={errors} touched={touched} isRequired />
<StringField name="name" label={t('common.name')} errors={errors} touched={touched} isRequired />
<SelectField
name="userRole"
label={t('user.role')}
errors={errors}
touched={touched}
options={[
{ value: 'accounting', label: 'Accounting' },
{ value: 'admin', label: 'Admin' },
{ value: 'csr', label: 'CSR' },
{ value: 'installer', label: 'Installer' },
{ value: 'noc', label: 'NOC' },
{ value: 'root', label: 'Root' },
{ value: 'system', label: 'System' },
]}
isRequired
/>
<StringField
name="currentPassword"
label={t('user.password')}
errors={errors}
touched={touched}
isRequired
hideButton
/>
<ToggleField name="changePassword" label={t('users.change_password')} errors={errors} touched={touched} />
<ToggleField name="emailValidation" label={t('users.email_validation')} errors={errors} touched={touched} />
<StringField name="description" label={t('common.description')} errors={errors} touched={touched} />
<StringField name="note" label={t('common.note')} errors={errors} touched={touched} />
</SimpleGrid>
<Flex justifyContent="center" alignItems="center" maxW="100%" mt={4} mb={6}>
<Box w="100%">
<Link href={passwordPolicyLink} isExternal>
{t('login.password_policy')}
<ExternalLinkIcon mx="2px" />
</Link>
</Box>
</Flex>
</Form>
)}
</Formik>
);
};
CreateUserForm.propTypes = propTypes;
export default CreateUserForm;

View File

@@ -0,0 +1,197 @@
import React, { useEffect, useState } from 'react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Box, Flex, Link, useToast, SimpleGrid } from '@chakra-ui/react';
import axios from 'axios';
import { Formik, Form, FormikProps } from 'formik';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import * as Yup from 'yup';
import { SelectField } from '../../../../components/Form/Fields/SelectField';
import { StringField } from '../../../../components/Form/Fields/StringField';
import { ToggleField } from '../../../../components/Form/Fields/ToggleField';
import { useAuth } from 'contexts/AuthProvider';
import { testRegex } from 'helpers/formTests';
import { useCreateUser } from 'hooks/Network/Users';
import { useApiRequirements } from 'hooks/useApiRequirements';
export type CreateUserFormValues = {
name: string;
description: string;
email: string;
currentPassword: string;
note: string;
userRole: string;
emailValidation: boolean;
changePassword: boolean;
};
type Props = {
isOpen: boolean;
onClose: () => void;
formRef: React.Ref<FormikProps<CreateUserFormValues>>;
};
const CreateUserForm = ({ isOpen, onClose, formRef }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const { user } = useAuth();
const [formKey, setFormKey] = useState(uuid());
const createUser = useCreateUser();
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
const CreateUserSchema = Yup.object().shape({
email: Yup.string().email(t('form.invalid_email')).required('Required'),
name: Yup.string().required('Required'),
description: Yup.string(),
currentPassword: Yup.string()
.required(t('form.required'))
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern))
.default(''),
note: Yup.string(),
userRole: Yup.string(),
});
const CreateUserNonRootSchema = Yup.object().shape({
email: Yup.string().email(t('form.invalid_email')).required('Required'),
name: Yup.string().required('Required'),
description: Yup.string(),
currentPassword: Yup.string()
.required(t('form.required'))
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern))
.default(''),
note: Yup.string(),
userRole: Yup.string(),
});
const createParameters = ({
name,
description,
email,
currentPassword,
note,
userRole,
emailValidation,
changePassword,
}: CreateUserFormValues) => {
if (userRole === 'root') {
return {
name,
email,
currentPassword,
userRole,
description: description.length > 0 ? description : undefined,
notes: note.length > 0 ? [{ note }] : undefined,
emailValidation,
changePassword,
};
}
return {
name,
email,
currentPassword,
userRole,
description: description.length > 0 ? description : undefined,
notes: note.length > 0 ? [{ note }] : undefined,
emailValidation,
changePassword,
};
};
const defaultRole = () => {
if (user?.userRole === 'admin') return 'csr';
if (user) return user.userRole;
return 'csr';
};
useEffect(() => {
setFormKey(uuid());
}, [isOpen]);
return (
<Formik
innerRef={formRef}
key={formKey}
initialValues={
{
name: '',
description: '',
email: '',
currentPassword: '',
note: '',
userRole: defaultRole(),
changePassword: true,
emailValidation: true,
} as CreateUserFormValues
}
validationSchema={user?.userRole === 'root' ? CreateUserSchema : CreateUserNonRootSchema}
onSubmit={(formData, { setSubmitting, resetForm }) =>
createUser.mutate(createParameters(formData), {
onSuccess: () => {
setSubmitting(false);
resetForm();
toast({
id: 'user-creation-success',
title: t('common.success'),
description: t('crud.success_create_obj', {
obj: t('user.title'),
}),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
onClose();
},
onError: (e) => {
setSubmitting(false);
if (axios.isAxiosError(e))
toast({
id: uuid(),
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
})
}
>
<Form>
<SimpleGrid minChildWidth="300px" spacing="20px">
<StringField name="email" label={t('common.email')} isRequired />
<StringField name="name" label={t('common.name')} isRequired />
<SelectField
name="userRole"
label={t('user.role')}
options={[
{ value: 'accounting', label: 'Accounting' },
{ value: 'admin', label: 'Admin' },
{ value: 'csr', label: 'CSR' },
{ value: 'installer', label: 'Installer' },
{ value: 'noc', label: 'NOC' },
{ value: 'root', label: 'Root' },
{ value: 'system', label: 'System' },
]}
isRequired
/>
<StringField name="currentPassword" label={t('user.password')} isRequired hideButton />
<ToggleField name="changePassword" label={t('users.change_password')} />
<ToggleField name="emailValidation" label={t('users.email_validation')} />
<StringField name="description" label={t('common.description')} />
<StringField name="note" label={t('common.note')} />
</SimpleGrid>
<Flex justifyContent="center" alignItems="center" maxW="100%" mt={4} mb={6}>
<Box w="100%">
<Link href={passwordPolicyLink} isExternal>
{t('login.password_policy')}
<ExternalLinkIcon mx="2px" />
</Link>
</Box>
</Flex>
</Form>
</Formik>
);
};
export default CreateUserForm;

View File

@@ -1,96 +0,0 @@
import React from 'react';
import { AddIcon } from '@chakra-ui/icons';
import { Button, useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import CreateUserForm from './Form';
import { CloseButton } from 'components/Buttons/CloseButton';
import { SaveButton } from 'components/Buttons/SaveButton';
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
import { ModalHeader } from 'components/Modals/GenericModal/ModalHeader';
import { axiosSec } from 'constants/axiosInstances';
import { useAuth } from 'contexts/AuthProvider';
import { useFormRef } from 'hooks/useFormRef';
const propTypes = {
requirements: PropTypes.shape({
accessPolicy: PropTypes.string,
passwordPolicy: PropTypes.string,
}),
refreshUsers: PropTypes.func.isRequired,
};
const defaultProps = {
requirements: {
accessPolicy: '',
passwordPolicy: '',
},
};
const CreateUserModal = ({ requirements, refreshUsers }) => {
const { t } = useTranslation();
const { user } = useAuth();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
const { form, formRef } = useFormRef();
const createUser = useMutation((newUser) =>
axiosSec.post(`user/0${newUser.emailValidation ? '?email_verification=true' : ''}`, newUser),
);
const closeModal = () => (form.dirty ? openConfirm() : onClose());
const closeCancelAndForm = () => {
closeConfirm();
onClose();
};
return (
<>
<Button
hidden={user?.userRole === 'CSR'}
alignItems="center"
colorScheme="blue"
rightIcon={<AddIcon />}
onClick={onOpen}
ml={2}
>
{t('crud.create')}
</Button>
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
<ModalHeader
title={t('crud.create_object', { obj: t('user.title') })}
right={
<>
<SaveButton
onClick={form.submitForm}
isLoading={form.isSubmitting}
isDisabled={!form.isValid || !form.dirty}
/>
<CloseButton ml={2} onClick={closeModal} />
</>
}
/>
<ModalBody>
<CreateUserForm
requirements={requirements}
createUser={createUser}
isOpen={isOpen}
onClose={onClose}
refreshUsers={refreshUsers}
formRef={formRef}
/>
</ModalBody>
</ModalContent>
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
</Modal>
</>
);
};
CreateUserModal.propTypes = propTypes;
CreateUserModal.defaultProps = defaultProps;
export default CreateUserModal;

View File

@@ -0,0 +1,57 @@
import * as React from 'react';
import { AddIcon } from '@chakra-ui/icons';
import { Button, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { SaveButton } from '../../../../components/Buttons/SaveButton';
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
import { Modal } from '../../../../components/Modals/Modal';
import CreateUserForm, { CreateUserFormValues } from './Form';
import { useAuth } from 'contexts/AuthProvider';
import { useFormRef } from 'hooks/useFormRef';
const CreateUserModal = () => {
const { t } = useTranslation();
const { user } = useAuth();
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
const { form, formRef } = useFormRef<CreateUserFormValues>();
const closeModal = () => (form.dirty ? openConfirm() : onClose());
const closeCancelAndForm = () => {
closeConfirm();
onClose();
};
return (
<>
<Button
hidden={user?.userRole === 'csr'}
alignItems="center"
colorScheme="blue"
rightIcon={<AddIcon />}
onClick={onOpen}
ml={2}
>
{t('crud.create')}
</Button>
<Modal
isOpen={isOpen}
onClose={closeModal}
title={t('crud.create_object', { obj: t('user.title') })}
topRightButtons={
<SaveButton
onClick={form.submitForm}
isLoading={form.isSubmitting}
isDisabled={!form.isValid || !form.dirty}
/>
}
>
<CreateUserForm isOpen={isOpen} onClose={onClose} formRef={formRef} />
</Modal>
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
</>
);
};
export default CreateUserModal;

View File

@@ -1,64 +1,65 @@
import React, { useEffect, useState } from 'react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { Box, Flex, Link, useToast, Tabs, TabList, TabPanels, TabPanel, Tab, SimpleGrid } from '@chakra-ui/react';
import { Formik, Form } from 'formik';
import PropTypes from 'prop-types';
import axios from 'axios';
import { Formik, Form, FormikProps } from 'formik';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import * as Yup from 'yup';
import { NotesField } from 'components/Form/Fields/NotesField';
import { SelectField } from 'components/Form/Fields/SelectField';
import { StringField } from 'components/Form/Fields/StringField';
import { ToggleField } from 'components/Form/Fields/ToggleField';
import { NotesField } from '../../../../components/Form/Fields/NotesField';
import { SelectField } from '../../../../components/Form/Fields/SelectField';
import { StringField } from '../../../../components/Form/Fields/StringField';
import { useAuth } from 'contexts/AuthProvider';
import { testObjectName, testRegex } from 'helpers/formTests';
import { User, useUpdateUser } from 'hooks/Network/Users';
import { useApiRequirements } from 'hooks/useApiRequirements';
const UpdateUserSchema = (t, { passRegex }) =>
Yup.object().shape({
name: Yup.string().required(t('form.required')).test('len', t('common.name_error'), testObjectName),
currentPassword: Yup.string()
.notRequired()
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex)),
description: Yup.string(),
mfa: Yup.string(),
phoneNumber: Yup.string(),
});
const propTypes = {
editing: PropTypes.bool.isRequired,
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
updateUser: PropTypes.instanceOf(Object).isRequired,
refreshUsers: PropTypes.func.isRequired,
userToUpdate: PropTypes.shape({
email: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
currentPassword: PropTypes.string.isRequired,
userRole: PropTypes.string.isRequired,
}).isRequired,
formRef: PropTypes.instanceOf(Object).isRequired,
type Props = {
editing: boolean;
isOpen: boolean;
onClose: () => void;
selectedUser: User;
formRef: React.Ref<FormikProps<User>>;
};
const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, userToUpdate, formRef }) => {
const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const { user } = useAuth();
const [formKey, setFormKey] = useState(uuid());
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
const updateUser = useUpdateUser();
const UpdateUserSchema = () =>
Yup.object().shape({
name: Yup.string().required(t('form.required')).test('len', t('common.name_error'), testObjectName),
currentPassword: Yup.string()
.notRequired()
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern)),
description: Yup.string(),
mfa: Yup.string(),
phoneNumber: Yup.string(),
});
const formIsDisabled = () => {
if (!editing) return true;
if (user?.userRole === 'root') return false;
if (user?.userRole === 'partner') return false;
if (user?.userRole === 'admin') {
if (userToUpdate.userRole === 'partner' || userToUpdate.userRole === 'admin') return true;
if (selectedUser.userRole === 'root' || selectedUser.userRole === 'partner' || selectedUser.userRole === 'admin')
return true;
return false;
}
return true;
};
const canEditRole = () => {
if (selectedUser.userRole === 'root') return false;
if (user?.userRole === 'root') return true;
if (user?.userRole === 'admin' && selectedUser.userRole !== 'admin') return true;
return false;
};
useEffect(() => {
setFormKey(uuid());
}, [isOpen]);
@@ -68,18 +69,15 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
innerRef={formRef}
enableReinitialize
key={formKey}
initialValues={userToUpdate}
validationSchema={UpdateUserSchema(t, { passRegex: passwordPattern })}
onSubmit={(
{ name, description, currentPassword, userRole, notes, changePassword },
{ setSubmitting, resetForm },
) =>
initialValues={selectedUser}
validationSchema={UpdateUserSchema}
onSubmit={({ name, description, currentPassword, userRole, notes }, { setSubmitting, resetForm }) =>
updateUser.mutateAsync(
{
id: selectedUser.id,
name,
currentPassword: currentPassword.length > 0 ? currentPassword : undefined,
userRole,
changePassword,
description,
notes: notes.filter((note) => note.isNew),
},
@@ -98,23 +96,20 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
isClosable: true,
position: 'top-right',
});
refreshUsers();
onClose();
},
onError: (e) => {
setSubmitting(false);
if (axios.isAxiosError(e))
toast({
id: uuid(),
title: t('common.error'),
description: t('crud.error_update_obj', {
obj: t('user.title'),
e: e?.response?.data?.ErrorDescription,
}),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
setSubmitting(false);
},
},
)
@@ -144,10 +139,9 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
{ value: 'system', label: 'System' },
]}
isRequired
isDisabled
isDisabled={!canEditRole() || formIsDisabled()}
/>
<StringField name="name" label={t('common.name')} isDisabled={formIsDisabled()} isRequired />
<ToggleField name="changePassword" label={t('users.change_password')} isDisabled={formIsDisabled()} />
<StringField
name="currentPassword"
label={t('user.password')}
@@ -176,6 +170,4 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
);
};
UpdateUserForm.propTypes = propTypes;
export default UpdateUserForm;

View File

@@ -1,111 +0,0 @@
import React, { useEffect } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
useToast,
Spinner,
Center,
useDisclosure,
useBoolean,
} from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import UpdateUserForm from './Form';
import { CloseButton } from 'components/Buttons/CloseButton';
import { EditButton } from 'components/Buttons/EditButton';
import { SaveButton } from 'components/Buttons/SaveButton';
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
import { ModalHeader } from 'components/Modals/GenericModal/ModalHeader';
import { axiosSec } from 'constants/axiosInstances';
import { useGetUser } from 'hooks/Network/Users';
import { useFormRef } from 'hooks/useFormRef';
const propTypes = {
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
userId: PropTypes.string,
requirements: PropTypes.shape({
accessPolicy: PropTypes.string,
passwordPolicy: PropTypes.string,
}),
refreshUsers: PropTypes.func.isRequired,
};
const defaultProps = {
userId: '',
requirements: {
accessPolicy: '',
passwordPolicy: '',
},
};
const EditUserModal = ({ isOpen, onClose, userId, requirements, refreshUsers }) => {
const { t } = useTranslation();
const [editing, setEditing] = useBoolean();
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
const toast = useToast();
const { form, formRef } = useFormRef();
const canFetchUser = userId !== '' && isOpen;
const { data: user, isLoading } = useGetUser({ t, toast, id: userId, enabled: canFetchUser });
const createUser = useMutation((userInfo) => axiosSec.put(`user/${userId}`, userInfo));
const closeModal = () => (form.dirty ? openConfirm() : onClose());
const closeCancelAndForm = () => {
closeConfirm();
onClose();
};
useEffect(() => {
if (isOpen) setEditing.off();
}, [isOpen]);
return (
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
<ModalHeader
title={t('crud.edit_obj', { obj: t('user.title') })}
right={
<>
<SaveButton
onClick={form.submitForm}
isLoading={form.isSubmitting}
isDisabled={!editing || !form.isValid || !form.dirty}
/>
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
<CloseButton ml={2} onClick={closeModal} />
</>
}
/>
<ModalBody>
{!isLoading && user ? (
<UpdateUserForm
editing={editing}
userToUpdate={user}
requirements={requirements}
updateUser={createUser}
isOpen={isOpen}
onClose={onClose}
refreshUsers={refreshUsers}
formRef={formRef}
/>
) : (
<Center>
<Spinner />
</Center>
)}
</ModalBody>
</ModalContent>
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
</Modal>
);
};
EditUserModal.propTypes = propTypes;
EditUserModal.defaultProps = defaultProps;
export default EditUserModal;

View File

@@ -0,0 +1,68 @@
import * as React from 'react';
import { useEffect } from 'react';
import { Spinner, Center, useDisclosure, useBoolean } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { EditButton } from '../../../../components/Buttons/EditButton';
import { SaveButton } from '../../../../components/Buttons/SaveButton';
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
import { Modal } from '../../../../components/Modals/Modal';
import UpdateUserForm from './Form';
import { useGetUser, User } from 'hooks/Network/Users';
import { useFormRef } from 'hooks/useFormRef';
type Props = {
userId?: string;
isOpen: boolean;
onClose: () => void;
};
const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
const { t } = useTranslation();
const [editing, setEditing] = useBoolean();
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
const { form, formRef } = useFormRef<User>();
const canFetchUser = userId !== '' && isOpen;
const { data: user, isFetching } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
const closeModal = () => (form.dirty ? openConfirm() : onClose());
const closeCancelAndForm = () => {
closeConfirm();
onClose();
};
useEffect(() => {
if (isOpen) setEditing.off();
}, [isOpen]);
return (
<>
<Modal
isOpen={isOpen}
onClose={closeModal}
title={t('crud.edit_obj', { obj: t('user.title') })}
topRightButtons={
<>
<SaveButton
onClick={form.submitForm}
isLoading={form.isSubmitting}
isDisabled={!editing || !form.isValid || !form.dirty}
/>
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
</>
}
>
{!isFetching && user ? (
<UpdateUserForm editing={editing} selectedUser={user} isOpen={isOpen} onClose={onClose} formRef={formRef} />
) : (
<Center>
<Spinner />
</Center>
)}
</Modal>
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
</>
);
};
export default EditUserModal;

View File

@@ -17,37 +17,29 @@ import {
useDisclosure,
useToast,
} from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
import { MagnifyingGlass, Trash } from 'phosphor-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import ActionsDropdown from './ActionsDropdown';
import { axiosSec } from 'constants/axiosInstances';
import { useDeleteUser, User } from 'hooks/Network/Users';
const deleteUserApi = async (userId) => axiosSec.delete(`/user/${userId}`).then(() => true);
const propTypes = {
cell: PropTypes.shape({
original: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
suspended: PropTypes.bool.isRequired,
waitingForEmailCheck: PropTypes.bool.isRequired,
}).isRequired,
}).isRequired,
refreshTable: PropTypes.func.isRequired,
openEdit: PropTypes.func.isRequired,
type Props = {
user: User;
openEdit: (user: User) => void;
refreshTable: () => void;
};
const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
const UserActions = ({ user, openEdit, refreshTable }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const { isOpen, onOpen, onClose } = useDisclosure();
const deleteUser = useMutation(() => deleteUserApi(user.id), {
const deleteUser = useDeleteUser();
const handleDeleteClick = () =>
deleteUser.mutate(user.id, {
onSuccess: () => {
onClose();
refreshTable();
toast({
id: `user-delete-success${uuid()}`,
title: t('common.success'),
@@ -61,13 +53,11 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
});
},
onError: (e) => {
if (axios.isAxiosError(e))
toast({
id: 'user-delete-error',
title: t('common.error'),
description: t('crud.error_delete_obj', {
obj: user.name,
e: e?.response?.data?.ErrorDescription,
}),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
@@ -76,8 +66,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
},
});
const handleDeleteClick = () => deleteUser.mutateAsync();
const handleEditClick = () => openEdit(user.id);
const handleEditClick = () => openEdit(user);
return (
<Flex>
@@ -85,7 +74,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
<Box>
<PopoverTrigger>
<IconButton colorScheme="red" icon={<Trash size={20} />} size="sm" />
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
</PopoverTrigger>
</Box>
</Tooltip>
@@ -97,10 +86,10 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
<PopoverFooter>
<Center>
<Button colorScheme="gray" mr="1" onClick={onClose}>
Cancel
{t('common.cancel')}
</Button>
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteUser.isLoading}>
Yes
{t('common.yes')}
</Button>
</Center>
</PopoverFooter>
@@ -114,6 +103,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
/>
<Tooltip hasArrow label={t('common.view_details')} placement="top">
<IconButton
aria-label={t('common.view_details')}
ml={2}
colorScheme="blue"
icon={<MagnifyingGlass size={20} />}
@@ -125,6 +115,4 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
);
};
UserActions.propTypes = propTypes;
export default UserActions;

View File

@@ -1,67 +1,55 @@
import React, { useCallback, useState } from 'react';
import { Avatar, Box, Button, Flex, Heading, useDisclosure, useToast } from '@chakra-ui/react';
import { Avatar, Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
import { ArrowsClockwise } from 'phosphor-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
import { DataTable } from '../../../components/DataTables/DataTable';
import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
import CreateUserModal from './CreateUserModal';
import EditUserModal from './EditUserModal';
import UserActions from './UserActions';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import { CardHeader } from 'components/Containers/Card/CardHeader';
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
import { DataTable } from 'components/DataTables/DataTable';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { useAuth } from 'contexts/AuthProvider';
import { useGetRequirements } from 'hooks/Network/Requirements';
import { useGetUsers } from 'hooks/Network/Users';
import { Column } from 'models/Table';
import { User } from 'models/User';
const propTypes = {
title: PropTypes.string,
};
const defaultProps = {
title: null,
};
const UserTable = ({ title }) => {
const UserTable = () => {
const { t } = useTranslation();
const toast = useToast();
const { user } = useAuth();
const [usersWithAvatars, setUsersWithAvatars] = useState([]);
const { data: requirements } = useGetRequirements();
const [editId, setEditId] = useState('');
const [hiddenColumns, setHiddenColumns] = useState([]);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers({ t, toast, setUsersWithAvatars });
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
const openEditModal = (userId) => {
setEditId(userId);
const openEditModal = (editUser: User) => {
setEditId(editUser.id);
openEdit();
};
const memoizedActions = useCallback(
(cell) => <UserActions cell={cell.row} refreshTable={refreshUsers} key={uuid()} openEdit={openEditModal} />,
(userActions: User) => (
<UserActions user={userActions} refreshTable={refreshUsers} key={uuid()} openEdit={openEditModal} />
),
[],
);
const memoizedDate = useCallback((cell) => <FormattedDate date={cell.row.values.lastLogin} key={uuid()} />, []);
const memoizedDate = useCallback((date: number) => <FormattedDate date={date} key={uuid()} />, []);
const memoizedAvatar = useCallback(
(cell) => <Avatar name={cell.row.values.name} src={cell.row.original.avatar} />,
[],
);
const memoizedAvatar = useCallback((name: string, avatar: string) => <Avatar name={name} src={avatar} />, []);
// Columns array. This array contains your table headings and accessors which maps keys from data array
const columns = React.useMemo(() => {
const baseColumns = [
const baseColumns: Column<User>[] = [
{
id: 'avatar',
Header: t('account.avatar'),
Footer: '',
accessor: 'avatar',
customWidth: '32px',
Cell: ({ cell }) => memoizedAvatar(cell),
Cell: ({ cell }) => memoizedAvatar(cell.row.original.name, cell.row.original.avatar),
disableSortBy: true,
alwaysShow: true,
},
@@ -97,7 +85,7 @@ const UserTable = ({ title }) => {
Header: t('users.last_login'),
Footer: '',
accessor: 'lastLogin',
Cell: ({ cell }) => memoizedDate(cell, 'lastLogin'),
Cell: ({ cell }) => memoizedDate(cell.row.original.lastLogin),
customMinWidth: '150px',
customWidth: '150px',
},
@@ -116,7 +104,7 @@ const UserTable = ({ title }) => {
Footer: '',
accessor: 'Id',
customWidth: '80px',
Cell: ({ cell }) => memoizedActions(cell),
Cell: ({ cell }) => memoizedActions(cell.row.original),
disableSortBy: true,
alwaysShow: true,
});
@@ -124,30 +112,22 @@ const UserTable = ({ title }) => {
return baseColumns;
}, [t, user]);
const showUsers = () => {
if (usersWithAvatars.length > 0) return usersWithAvatars;
return users ?? [];
};
return (
<>
<Card>
<CardHeader mb="10px">
<Box>
<Heading size="md">{title}</Heading>
</Box>
<Flex w="100%" flexDirection="row" alignItems="center">
<Box ms="auto">
<ColumnPicker
columns={columns}
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
setHiddenColumns={setHiddenColumns}
preference="provisioning.userTable.hiddenColumns"
/>
<CreateUserModal requirements={requirements} refreshUsers={refreshUsers} />
<CreateUserModal />
<Button
colorScheme="gray"
onClick={refreshUsers}
onClick={() => refreshUsers()}
rightIcon={<ArrowsClockwise />}
ml={2}
isLoading={isFetching}
@@ -160,27 +140,20 @@ const UserTable = ({ title }) => {
<CardBody>
<Box overflowX="auto" w="100%">
<DataTable
columns={columns}
data={showUsers()}
columns={columns as Column<object>[]}
data={users?.filter((curr) => curr.email !== user?.email) ?? []}
isLoading={isFetching}
obj={t('users.title')}
sortBy={[{ id: 'email', desc: false }]}
hiddenColumns={hiddenColumns}
fullScreen
/>
</Box>
</CardBody>
</Card>
<EditUserModal
isOpen={editOpen}
onClose={closeEdit}
userId={editId}
requirements={requirements}
refreshUsers={refreshUsers}
/>
<EditUserModal isOpen={editOpen} onClose={closeEdit} userId={editId} />
</>
);
};
UserTable.propTypes = propTypes;
UserTable.defaultProps = defaultProps;
export default UserTable;