New login, create user form, user list components

This commit is contained in:
bourquecharles
2021-07-19 19:23:09 -04:00
parent 235f9d6a98
commit f9718d28e5
15 changed files with 374 additions and 49 deletions

View File

@@ -18,7 +18,6 @@ import {
import PropTypes from 'prop-types';
import LoadingButton from 'components/LoadingButton';
import CIcon from '@coreui/icons-react';
import styles from './index.module.scss';
const CreateUserForm = ({ t, fields, updateField, createUser, loading, policies }) => {
const [showPassword, setShowPassword] = useState(false);
@@ -129,7 +128,7 @@ const CreateUserForm = ({ t, fields, updateField, createUser, loading, policies
</CFormGroup>
<CRow>
<CCol />
<CCol xs={3} className={styles.linksColumn}>
<CCol xs={3} className="mt-2 text-right">
<CLink
className="c-subheader-nav-link"
aria-current="page"
@@ -150,13 +149,13 @@ const CreateUserForm = ({ t, fields, updateField, createUser, loading, policies
{t('common.password_policy')}
</CLink>
</CCol>
<CCol xs={2}>
<CCol xs={1} className="text-right">
<LoadingButton
label={t('user.create')}
isLoadingLabel={t('common.loading_ellipsis')}
isLoading={loading}
action={createUser}
block
block={false}
disabled={loading}
/>
</CCol>

View File

@@ -1,4 +0,0 @@
.linksColumn {
text-align: right;
padding-top: 5px;
}

View File

@@ -0,0 +1,19 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { CImg } from '@coreui/react';
const ImgWithFallback = ({ src, fallback }) => {
const [error, setError] = useState(false);
if (error) {
return <div className="avatar bg-secondary">{fallback()}</div>;
}
return <CImg className="avatar" src={src} onError={() => setError(true)} />;
};
ImgWithFallback.propTypes = {
src: PropTypes.string.isRequired,
fallback: PropTypes.func.isRequired,
};
export default React.memo(ImgWithFallback);

View File

@@ -42,4 +42,4 @@ LoadingButton.defaultProps = {
disabled: false,
};
export default LoadingButton;
export default React.memo(LoadingButton);

View File

@@ -12,6 +12,7 @@ import {
CPopover,
CAlert,
CInvalidFeedback,
CLink,
} from '@coreui/react';
import PropTypes from 'prop-types';
import CIcon from '@coreui/icons-react';
@@ -29,6 +30,7 @@ const ChangePasswordForm = ({
updateField,
changePasswordResponse,
cancelPasswordChange,
policies,
}) => (
<CForm onKeyDown={(e) => onKeyDown(e, signIn)}>
<h1>
@@ -88,13 +90,31 @@ const ChangePasswordForm = ({
</CCol>
</CRow>
<CRow>
<CCol xs="6">
<CCol>
<CButton color="primary" className="px-4" onClick={() => signIn(true)} disabled={loading}>
{loading ? t('login.changing_password') : t('login.change_password')}
<CSpinner hidden={!loading} color="light" component="span" size="sm" />
</CButton>
<CLink
className="c-subheader-nav-link px-3"
aria-current="page"
href={policies.accessPolicy}
target="_blank"
hidden={policies.accessPolicy.length === 0}
>
{t('common.access_policy')}
</CLink>
<CLink
className="c-subheader-nav-link"
aria-current="page"
href={policies.passwordPolicy}
target="_blank"
hidden={policies.passwordPolicy.length === 0}
>
{t('common.password_policy')}
</CLink>
</CCol>
<CCol xs="6" className={styles.forgotPassword}>
<CCol xs="5" className={styles.forgotPassword}>
<CButton variant="ghost" color="primary" onClick={cancelPasswordChange}>
{t('common.cancel')}
</CButton>
@@ -113,6 +133,7 @@ ChangePasswordForm.propTypes = {
updateField: PropTypes.func.isRequired,
changePasswordResponse: PropTypes.instanceOf(Object).isRequired,
cancelPasswordChange: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
};
export default ChangePasswordForm;
export default React.memo(ChangePasswordForm);

View File

@@ -12,6 +12,7 @@ import {
CPopover,
CAlert,
CInvalidFeedback,
CLink,
} from '@coreui/react';
import PropTypes from 'prop-types';
import CIcon from '@coreui/icons-react';
@@ -29,6 +30,7 @@ const ForgotPasswordForm = ({
forgotResponse,
updateField,
toggleForgotPassword,
policies,
}) => (
<CForm onKeyDown={(e) => onKeyDown(e, sendForgotPasswordEmail)}>
<h1>
@@ -87,7 +89,7 @@ const ForgotPasswordForm = ({
</CCol>
</CRow>
<CRow>
<CCol xs="6">
<CCol>
<CButton
color="primary"
className="px-4"
@@ -97,8 +99,26 @@ const ForgotPasswordForm = ({
{loading ? t('login.sending_ellipsis') : t('login.send_forgot')}
<CSpinner hidden={!loading} color="light" component="span" size="sm" />
</CButton>
<CLink
className="c-subheader-nav-link px-3"
aria-current="page"
href={policies.accessPolicy}
target="_blank"
hidden={policies.accessPolicy.length === 0}
>
{t('common.access_policy')}
</CLink>
<CLink
className="c-subheader-nav-link"
aria-current="page"
href={policies.passwordPolicy}
target="_blank"
hidden={policies.passwordPolicy.length === 0}
>
{t('common.password_policy')}
</CLink>
</CCol>
<CCol xs="6" className={styles.forgotPassword}>
<CCol xs="5" className={styles.forgotPassword}>
<CButton variant="ghost" color="primary" onClick={toggleForgotPassword}>
{t('common.back_to_login')}
</CButton>
@@ -117,6 +137,7 @@ ForgotPasswordForm.propTypes = {
fields: PropTypes.instanceOf(Object).isRequired,
updateField: PropTypes.func.isRequired,
toggleForgotPassword: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
};
export default ForgotPasswordForm;
export default React.memo(ForgotPasswordForm);

View File

@@ -12,6 +12,7 @@ import {
CPopover,
CAlert,
CInvalidFeedback,
CLink,
} from '@coreui/react';
import PropTypes from 'prop-types';
import CIcon from '@coreui/icons-react';
@@ -29,6 +30,7 @@ const LoginForm = ({
updateField,
loginResponse,
toggleForgotPassword,
policies,
}) => (
<CForm onKeyDown={(e) => onKeyDown(e, signIn)}>
<h1>
@@ -105,13 +107,31 @@ const LoginForm = ({
</CCol>
</CRow>
<CRow>
<CCol xs="6">
<CCol>
<CButton color="primary" className="px-4" onClick={signIn} disabled={loading}>
{loading ? t('login.logging_in') : t('login.login')}
<CSpinner hidden={!loading} color="light" component="span" size="sm" />
</CButton>
<CLink
className="c-subheader-nav-link px-3"
aria-current="page"
href={policies.accessPolicy}
target="_blank"
hidden={policies.accessPolicy.length === 0}
>
{t('common.access_policy')}
</CLink>
<CLink
className="c-subheader-nav-link"
aria-current="page"
href={policies.passwordPolicy}
target="_blank"
hidden={policies.passwordPolicy.length === 0}
>
{t('common.password_policy')}
</CLink>
</CCol>
<CCol xs="6" className={styles.forgotPassword}>
<CCol xs="5" className="text-right">
<CButton variant="ghost" color="primary" onClick={toggleForgotPassword}>
{t('common.forgot_password')}
</CButton>
@@ -130,6 +150,7 @@ LoginForm.propTypes = {
updateField: PropTypes.func.isRequired,
loginResponse: PropTypes.instanceOf(Object).isRequired,
toggleForgotPassword: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
};
export default LoginForm;
export default React.memo(LoginForm);

View File

@@ -22,6 +22,7 @@ const LoginPage = ({
sendForgotPasswordEmail,
changePasswordResponse,
cancelPasswordChange,
policies,
}) => {
const getForm = () => {
if (!isLogin) {
@@ -37,6 +38,7 @@ const LoginPage = ({
forgotResponse={forgotResponse}
toggleForgotPassword={toggleForgotPassword}
sendForgotPasswordEmail={sendForgotPasswordEmail}
policies={policies}
/>
);
}
@@ -52,6 +54,7 @@ const LoginPage = ({
updateField={updateField}
changePasswordResponse={changePasswordResponse}
cancelPasswordChange={cancelPasswordChange}
policies={policies}
/>
);
}
@@ -66,6 +69,7 @@ const LoginPage = ({
updateField={updateField}
loginResponse={loginResponse}
toggleForgotPassword={toggleForgotPassword}
policies={policies}
/>
);
};
@@ -81,7 +85,7 @@ const LoginPage = ({
alt="OpenWifi"
/>
<CCardGroup>
<CCard className="p-4">
<CCard className="p-4" color={isLogin && isPasswordChange ? 'secondary' : ''}>
<CCardBody>{getForm()}</CCardBody>
</CCard>
</CCardGroup>
@@ -108,6 +112,7 @@ LoginPage.propTypes = {
sendForgotPasswordEmail: PropTypes.func.isRequired,
changePasswordResponse: PropTypes.instanceOf(Object).isRequired,
cancelPasswordChange: PropTypes.func.isRequired,
policies: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(LoginPage);

View File

@@ -11,8 +11,9 @@ import {
CDataTable,
CPopover,
CButton,
CLink,
} from '@coreui/react';
import { cilBan, cilCheckCircle, cilTrash } from '@coreui/icons';
import { cilBan, cilCheckCircle, cilInfo, cilPlus, cilTrash } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { capitalizeFirstLetter, prettyDate } from '../../utils/formatting';
import DeleteModal from '../DeleteModal';
@@ -49,13 +50,13 @@ const UserListTable = ({
{ key: 'email', label: t('user.login_id'), _style: { width: '20%' } },
{ key: 'name', label: t('user.name'), _style: { width: '20%' } },
{ key: 'userRole', label: t('user.user_role'), _style: { width: '5%' } },
{ key: 'description', label: t('user.description'), _style: { width: '26%' } },
{ key: 'description', label: t('user.description'), _style: { width: '25%' } },
{ key: 'validated', label: t('user.validated'), _style: { width: '5%' } },
{ key: 'lastLogin', label: t('user.last_login'), _style: { width: '20%' } },
{
key: 'user_actions',
label: '',
_style: { width: '4%' },
_style: { width: '5%' },
sorter: false,
filter: false,
},
@@ -88,9 +89,26 @@ const UserListTable = ({
loading={loading}
hover
border
columnHeaderSlot={{
user_actions: (
<div className="text-center">
<CPopover content={t('user.create')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/users/create`}
>
<CButton color="primary" variant="outline" shape="square" size="sm">
<CIcon name="cil-info" content={cilPlus} size="sm" />
</CButton>
</CLink>
</CPopover>
</div>
),
}}
scopedSlots={{
validated: (item) => (
<td className="centeredColumn">
<td className="text-center">
<CPopover
content={item.validated ? t('user.validated') : t('user.not_validated')}
>
@@ -103,17 +121,34 @@ const UserListTable = ({
<td>{item.userRole ? capitalizeFirstLetter(item.userRole) : ''}</td>
),
user_actions: (item) => (
<td className="py-2">
<CPopover content={t('common.delete')}>
<CButton
onClick={() => handleDeleteClick(item.Id)}
color="primary"
variant="outline"
size="sm"
>
<CIcon content={cilTrash} size="sm" />
</CButton>
</CPopover>
<td className="py-2 text-center">
<CRow>
<CCol>
<CPopover content={t('configuration.details')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/users/${item.Id}`}
>
<CButton color="primary" variant="outline" shape="square" size="sm">
<CIcon name="cil-info" content={cilInfo} size="sm" />
</CButton>
</CLink>
</CPopover>
</CCol>
<CCol>
<CPopover content={t('common.delete')}>
<CButton
onClick={() => handleDeleteClick(item.Id)}
color="primary"
variant="outline"
size="sm"
>
<CIcon content={cilTrash} size="sm" />
</CButton>
</CPopover>
</CCol>
</CRow>
</td>
),
}}

View File

@@ -0,0 +1,139 @@
import React, { useState } from 'react';
import {
CButton,
CCard,
CCardBody,
CCardHeader,
CCol,
CForm,
CFormGroup,
CInput,
CInputGroup,
CInputGroupAppend,
CInvalidFeedback,
CLabel,
CPopover,
CRow,
CSelect,
CSwitch,
} from '@coreui/react';
import PropTypes from 'prop-types';
import LoadingButton from 'components/LoadingButton';
import CIcon from '@coreui/icons-react';
const CreateUserForm = ({ t, user, updateUserWithId, loading, saveUser }) => {
const [showPassword, setShowPassword] = useState(false);
const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
return (
<CCard>
<CCardHeader>{t('common.details')}</CCardHeader>
<CCardBody>
<CForm>
<CFormGroup row>
<CCol>
<CLabel htmlFor="email">{t('user.email_address')}</CLabel>
<p className="form-control-static mt-2">{user.email.value}</p>
</CCol>
<CCol>
<CLabel htmlFor="name">{t('user.name')}</CLabel>
<CInput
id="name"
value={user.name.value}
onChange={updateUserWithId}
maxLength="20"
/>
</CCol>
<CCol>
<CLabel htmlFor="changePassword">{t('user.force_password_change')}</CLabel>
<CInputGroup>
<CSwitch
id="changePassword"
color="success"
defaultChecked={user.changePassword.value}
onClick={updateUserWithId}
size="lg"
/>
</CInputGroup>
</CCol>
</CFormGroup>
<CFormGroup row>
<CCol>
<CLabel htmlFor="userRole">{t('user.user_role')}</CLabel>
<CSelect custom id="userRole" defaultValue="Admin" onChange={updateUserWithId}>
<option value="admin">Admin</option>
<option value="csr">CSR</option>
<option value="root">Root</option>
<option value="special">Special</option>
<option value="sub">Sub</option>
<option value="system">System</option>
</CSelect>
</CCol>
<CCol>
<CLabel htmlFor="newPascurrentPasswordsword">{t('login.new_password')}</CLabel>
<CInputGroup>
<CInput
type={showPassword ? 'text' : 'password'}
id="currentPassword"
value={user.currentPassword.value}
onChange={updateUserWithId}
invalid={user.currentPassword.error}
maxLength="50"
/>
<CInputGroupAppend>
<CPopover content={t('user.show_hide_password')}>
<CButton type="button" onClick={toggleShowPassword} color="secondary">
<CIcon
name={showPassword ? 'cil-envelope-open' : 'cil-envelope-closed'}
size="sm"
/>
</CButton>
</CPopover>
</CInputGroupAppend>
<CInvalidFeedback>{t('user.provide_password')}</CInvalidFeedback>
</CInputGroup>
</CCol>
</CFormGroup>
<CFormGroup row>
<CCol>
<CLabel htmlFor="description">{t('user.description')}</CLabel>
<CInput
id="description"
value={user.description.value}
onChange={updateUserWithId}
maxLength="50"
/>
</CCol>
<CCol />
</CFormGroup>
<CRow>
<CCol />
<CCol xs={3} className="text-right">
<LoadingButton
label={t('common.save')}
isLoadingLabel={t('common.saving')}
isLoading={loading}
action={saveUser}
block={false}
disabled={loading}
/>
</CCol>
</CRow>
</CForm>
</CCardBody>
</CCard>
);
};
CreateUserForm.propTypes = {
t: PropTypes.func.isRequired,
user: PropTypes.instanceOf(Object).isRequired,
updateUserWithId: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
saveUser: PropTypes.func.isRequired,
};
export default React.memo(CreateUserForm);

View File

@@ -0,0 +1,31 @@
import { useState } from 'react';
export default (initialState) => {
const [user, setUser] = useState(initialState);
return [
user,
(e) => {
setUser({
...user,
[e.target.id]: {
...user[e.target.id],
value: e.target.value,
error: false,
},
});
},
(key, newValues) => {
setUser({
...user,
[user]: {
...user[key],
...newValues,
},
});
},
(newUser) => {
setUser(newUser);
},
];
};

View File

@@ -10,9 +10,11 @@ export { default as UserListTable } from './components/UserListTable';
export { default as CreateUserForm } from './components/CreateUserForm';
export { default as LoadingButton } from './components/LoadingButton';
export { default as ConfirmFooter } from './components/ConfirmFooter';
export { default as UserProfileCard } from './components/UserProfileCard';
// Pages
export { default as LoginPage } from './components/LoginPage';
// Hooks
export { default as useFormFields } from './hooks/FormFields';
export { default as useFormFields } from './hooks/useFormFields';
export { default as useUser } from './hooks/useUser';

View File

@@ -6,15 +6,29 @@ import {
CHeaderNav,
CSubheader,
CBreadcrumbRouter,
CLink,
CPopover,
CDropdown,
CDropdownToggle,
CDropdownMenu,
CDropdownItem,
} from '@coreui/react';
import PropTypes from 'prop-types';
import CIcon from '@coreui/icons-react';
import { cilAccountLogout } from '@coreui/icons';
import LanguageSwitcher from '../../components/LanguageSwitcher';
import ImgWithFallback from '../../components/ImgWithFallback';
import { emailToName } from '../../utils/formatting';
const Header = ({ showSidebar, setShowSidebar, routes, t, i18n, logout, authToken, endpoints }) => {
const Header = ({
showSidebar,
setShowSidebar,
routes,
t,
i18n,
logout,
authToken,
endpoints,
user,
}) => {
const [translatedRoutes, setTranslatedRoutes] = useState(routes);
const toggleSidebar = () => {
@@ -45,17 +59,26 @@ const Header = ({ showSidebar, setShowSidebar, routes, t, i18n, logout, authToke
<LanguageSwitcher i18n={i18n} />
</CHeaderNav>
<CHeaderNav className="px-3">
<CPopover content={t('common.logout')}>
<CLink className="c-subheader-nav-link">
<CIcon
name="cilAccountLogout"
content={cilAccountLogout}
size="2xl"
onClick={() => logout(authToken, endpoints.ucentralsec)}
/>
</CLink>
</CPopover>
<CHeaderNav className="px-1">
<CDropdown inNav className="c-header-nav-items mx-2" direction="down">
<CDropdownToggle className="c-header-nav-link" caret={false}>
<div className="c-avatar">
<ImgWithFallback
src={user.avatar && user.avatar !== '' ? user.avatar : '/'}
fallback={() => emailToName(user.email)}
/>
</div>
</CDropdownToggle>
<CDropdownMenu className="pt-0" placement="bottom-end">
<CDropdownItem>
<div className="px-3">My Account</div>
</CDropdownItem>
<CDropdownItem onClick={() => logout(authToken, endpoints.ucentralsec)}>
<strong className="px-3">Logout</strong>
<CIcon name="cilAccountLogout" content={cilAccountLogout} />
</CDropdownItem>
</CDropdownMenu>
</CDropdown>
</CHeaderNav>
<CSubheader className="px-3 justify-content-between">
@@ -77,6 +100,7 @@ Header.propTypes = {
logout: PropTypes.func.isRequired,
authToken: PropTypes.string.isRequired,
endpoints: PropTypes.instanceOf(Object).isRequired,
user: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(Header);

View File

@@ -17,3 +17,15 @@ export const prettyDate = (dateString) => {
};
export const capitalizeFirstLetter = (string) => string.charAt(0).toUpperCase() + string.slice(1);
export const emailToName = (email) => {
if (email) {
const pre = email.split('@')[0];
if (!pre.includes('.')) {
return `${pre.substring(0, 2).toUpperCase()}`;
}
const parts = pre.split('.');
return `${parts[0].charAt(0).toUpperCase()}${parts[1].charAt(0).toUpperCase()}`;
}
return 'N/A';
};