diff --git a/src/components/CreateUserForm/index.js b/src/components/CreateUserForm/index.js index 559eeec..f64d214 100644 --- a/src/components/CreateUserForm/index.js +++ b/src/components/CreateUserForm/index.js @@ -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 - + - + diff --git a/src/components/CreateUserForm/index.module.scss b/src/components/CreateUserForm/index.module.scss deleted file mode 100644 index 01089dc..0000000 --- a/src/components/CreateUserForm/index.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.linksColumn { - text-align: right; - padding-top: 5px; -} diff --git a/src/components/ImgWithFallback/index.js b/src/components/ImgWithFallback/index.js new file mode 100644 index 0000000..7e24189 --- /dev/null +++ b/src/components/ImgWithFallback/index.js @@ -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
{fallback()}
; + } + + return setError(true)} />; +}; + +ImgWithFallback.propTypes = { + src: PropTypes.string.isRequired, + fallback: PropTypes.func.isRequired, +}; +export default React.memo(ImgWithFallback); diff --git a/src/components/LoadingButton/index.js b/src/components/LoadingButton/index.js index 0bdc56e..7867f52 100644 --- a/src/components/LoadingButton/index.js +++ b/src/components/LoadingButton/index.js @@ -42,4 +42,4 @@ LoadingButton.defaultProps = { disabled: false, }; -export default LoadingButton; +export default React.memo(LoadingButton); diff --git a/src/components/LoginPage/ChangePasswordForm.js b/src/components/LoginPage/ChangePasswordForm.js index 22bec25..0c1b391 100644 --- a/src/components/LoginPage/ChangePasswordForm.js +++ b/src/components/LoginPage/ChangePasswordForm.js @@ -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, }) => ( onKeyDown(e, signIn)}>

@@ -88,13 +90,31 @@ const ChangePasswordForm = ({ - + signIn(true)} disabled={loading}> {loading ? t('login.changing_password') : t('login.change_password')} + + - + {t('common.cancel')} @@ -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); diff --git a/src/components/LoginPage/ForgotPasswordForm.js b/src/components/LoginPage/ForgotPasswordForm.js index b6dd803..4582aaf 100644 --- a/src/components/LoginPage/ForgotPasswordForm.js +++ b/src/components/LoginPage/ForgotPasswordForm.js @@ -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, }) => ( onKeyDown(e, sendForgotPasswordEmail)}>

@@ -87,7 +89,7 @@ const ForgotPasswordForm = ({ - + + + - + {t('common.back_to_login')} @@ -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); diff --git a/src/components/LoginPage/LoginForm.js b/src/components/LoginPage/LoginForm.js index 0dad482..db784f5 100644 --- a/src/components/LoginPage/LoginForm.js +++ b/src/components/LoginPage/LoginForm.js @@ -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, }) => ( onKeyDown(e, signIn)}>

@@ -105,13 +107,31 @@ const LoginForm = ({ - + {loading ? t('login.logging_in') : t('login.login')} + + - + {t('common.forgot_password')} @@ -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); diff --git a/src/components/LoginPage/index.js b/src/components/LoginPage/index.js index 5269bba..9474185 100644 --- a/src/components/LoginPage/index.js +++ b/src/components/LoginPage/index.js @@ -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" /> - + {getForm()} @@ -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); diff --git a/src/components/UserListTable/index.js b/src/components/UserListTable/index.js index d591b3e..f22f942 100644 --- a/src/components/UserListTable/index.js +++ b/src/components/UserListTable/index.js @@ -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: ( +
+ + `/users/create`} + > + + + + + +
+ ), + }} scopedSlots={{ validated: (item) => ( - + @@ -103,17 +121,34 @@ const UserListTable = ({ {item.userRole ? capitalizeFirstLetter(item.userRole) : ''} ), user_actions: (item) => ( - - - handleDeleteClick(item.Id)} - color="primary" - variant="outline" - size="sm" - > - - - + + + + + `/users/${item.Id}`} + > + + + + + + + + + handleDeleteClick(item.Id)} + color="primary" + variant="outline" + size="sm" + > + + + + + ), }} diff --git a/src/components/UserProfileCard/index.js b/src/components/UserProfileCard/index.js new file mode 100644 index 0000000..3382025 --- /dev/null +++ b/src/components/UserProfileCard/index.js @@ -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 ( + + {t('common.details')} + + + + + {t('user.email_address')} +

{user.email.value}

+
+ + {t('user.name')} + + + + {t('user.force_password_change')} + + + + +
+ + + {t('user.user_role')} + + + + + + + + + + + {t('login.new_password')} + + + + + + + + + + {t('user.provide_password')} + + + + + + {t('user.description')} + + + + + + + + + + +
+
+
+ ); +}; + +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); diff --git a/src/hooks/FormFields/index.js b/src/hooks/useFormFields/index.js similarity index 100% rename from src/hooks/FormFields/index.js rename to src/hooks/useFormFields/index.js diff --git a/src/hooks/useUser/index.js b/src/hooks/useUser/index.js new file mode 100644 index 0000000..b16252a --- /dev/null +++ b/src/hooks/useUser/index.js @@ -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); + }, + ]; +}; diff --git a/src/index.js b/src/index.js index e2c48d4..7616a22 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; diff --git a/src/layout/Header/index.js b/src/layout/Header/index.js index dbdba7d..e2fff77 100644 --- a/src/layout/Header/index.js +++ b/src/layout/Header/index.js @@ -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 - - - - logout(authToken, endpoints.ucentralsec)} - /> - - + + + +
+ emailToName(user.email)} + /> +
+
+ + +
My Account
+
+ logout(authToken, endpoints.ucentralsec)}> + Logout + + +
+
@@ -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); diff --git a/src/utils/formatting.js b/src/utils/formatting.js index 87e117b..5b0c516 100644 --- a/src/utils/formatting.js +++ b/src/utils/formatting.js @@ -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'; +};