diff --git a/app/src/components/layout/header/EnvLabels.tsx b/app/src/components/layout/header/EnvLabels.tsx deleted file mode 100644 index be497c65a..000000000 --- a/app/src/components/layout/header/EnvLabels.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Theme } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme: Theme) => ({ - appPhaseTag: { - marginLeft: theme.spacing(0.5), - color: '#fcba19', - textTransform: 'uppercase', - fontSize: '0.875rem' - } -})); - -export const BetaLabel: React.FC = () => { - const classes = useStyles(); - - return ( - - Beta - - ); -}; diff --git a/app/src/components/layout/header/Header.tsx b/app/src/components/layout/header/Header.tsx index 83efeddda..6e4bb2518 100644 --- a/app/src/components/layout/header/Header.tsx +++ b/app/src/components/layout/header/Header.tsx @@ -1,6 +1,5 @@ -import { mdiHelpCircle } from '@mdi/js'; +import { mdiMenu } from '@mdi/js'; import Icon from '@mdi/react'; -import { Theme } from '@mui/material'; import AppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -8,181 +7,275 @@ import Dialog from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import IconButton from '@mui/material/IconButton'; -import OtherLink from '@mui/material/Link'; import Toolbar from '@mui/material/Toolbar'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; import Typography from '@mui/material/Typography'; -import { makeStyles } from '@mui/styles'; import headerImageLarge from 'assets/images/gov-bc-logo-horiz.png'; import headerImageSmall from 'assets/images/gov-bc-logo-vert.png'; -import { BetaLabel } from 'components/layout/header/EnvLabels'; import { AuthGuard, SystemRoleGuard, UnAuthGuard } from 'components/security/Guards'; import { SYSTEM_ROLE } from 'constants/roles'; +import { Link as RouterLink } from 'react-router-dom'; import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { LoggedInUserControls, NotLoggedInUserControls } from './UserControls'; - -const useStyles = makeStyles((theme: Theme) => ({ - govHeader: {}, - govHeaderToolbar: { - height: '80px', - backgroundColor: theme.palette.bcgovblue.main - }, - brand: { - display: 'flex', - flex: '0 0 auto', - alignItems: 'center', - overflow: 'hidden', - color: 'inherit', - textDecoration: 'none', - fontSize: '1.75rem', - '& img': { - marginTop: '-2px', - verticalAlign: 'middle' - }, - '& picture': { - marginRight: '1.25rem' - }, - '&:hover': { - textDecoration: 'none' - }, - '&:focus': { - outlineOffset: '6px' - } - }, - '@media (max-width: 1000px)': { - brand: { - fontSize: '1rem', - '& picture': { - marginRight: '1rem' - } - }, - wrapText: { - display: 'block' - } - }, - mainNav: { - backgroundColor: '#38598a' - }, - mainNavToolbar: { - '& a': { - display: 'block', - padding: theme.spacing(2), - color: 'inherit', - fontSize: '1rem', - textDecoration: 'none' - }, - '& a:hover': { - textDecoration: 'underline' - }, - '& a:first-child': { - marginLeft: theme.spacing(-2) - } - }, - '.MuiDialogContent-root': { - '& p + p': { - marginTop: theme.spacing(2) - } - } -})); +import { LoggedInUser, PublicViewUser } from './UserControls'; const Header: React.FC = () => { - const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(null); - const [openSupportDialog, setOpenSupportDialog] = useState(false); - const preventDefault = (event: React.SyntheticEvent) => event.preventDefault(); + const [open, setOpen] = useState(false); + const menuOpen = Boolean(anchorEl); + // Support Dialog const showSupportDialog = () => { - setOpenSupportDialog(true); + setOpen(true); + hideMobileMenu(); }; const hideSupportDialog = () => { - setOpenSupportDialog(false); + setOpen(false); + }; + + // Responsive Menu + const showMobileMenu = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const hideMobileMenu = () => { + setAnchorEl(null); + }; + + const BetaLabel = () => { + return Beta; + }; + + // Unauthenticated public view + const AppBrand = () => { + return ( + + + + + + {'Government + + + BioHub + + + + + + + ); }; + return ( <> - - - - - - - - - - {'Government - - - BioHub - - - - - - Home - - - - Submissions - - - - - Manage Users - - - + + + {/* Responsive Menu */} + + + + + + + + + + + + + Home + + + + Submissions + + + + + Manage Users + + + + Support + + + + + + + + + {/* Desktop Menu */} + + + + - - + + Home + + + + Submissions + + + + + Manage Users + + + - - + + + + + + + + + + - - Need Help? + + Contact Support - - For technical support or questions about this application, please contact:  - + + For technical support or questions about this application, please email ‌ + biohub@gov.bc.ca - - . + - A support representative will respond to your request shortly. - Log out - - + + ); }; -export const NotLoggedInUserControls = () => { - const { keycloakWrapper } = useContext(AuthStateContext); +// Unauthenticated public view +export const PublicViewUser = () => { + const { keycloakWrapper } = useAuthStateContext() const loginUrl = useMemo(() => keycloakWrapper?.getLoginUrl(), [keycloakWrapper]); return ( - + <> + + ); }; diff --git a/app/src/hooks/useAuthStateContext.tsx b/app/src/hooks/useAuthStateContext.tsx new file mode 100644 index 000000000..ad7f8adaf --- /dev/null +++ b/app/src/hooks/useAuthStateContext.tsx @@ -0,0 +1,19 @@ +import { useContext } from 'react'; +import { AuthStateContext, IAuthState } from '../contexts/authStateContext'; + +/** + * Returns an instance of `IAuthState` from `AuthStateContext`. + * + * @return {*} {IAuthState} + */ +export const useAuthStateContext = (): IAuthState => { + const context = useContext(AuthStateContext); + + if (!context) { + throw Error( + 'AuthStateContext is undefined, please verify you are calling useAuthStateContext() as child of an component.' + ); + } + + return context; +}; diff --git a/app/src/hooks/useKeycloakWrapper.tsx b/app/src/hooks/useKeycloakWrapper.tsx index 71915789f..c23519e39 100644 --- a/app/src/hooks/useKeycloakWrapper.tsx +++ b/app/src/hooks/useKeycloakWrapper.tsx @@ -8,7 +8,9 @@ import useDataLoader from './useDataLoader'; export enum SYSTEM_IDENTITY_SOURCE { BCEID_BUSINESS = 'BCEIDBUSINESS', BCEID_BASIC = 'BCEIDBASIC', - IDIR = 'IDIR' + IDIR = 'IDIR', + DATABASE = 'DATABASE', + UNVERIFIED = 'UNVERIFIED' } export interface IUserInfo { diff --git a/app/src/utils/Utils.test.ts b/app/src/utils/Utils.test.ts index e84ea7bff..67c12bf72 100644 --- a/app/src/utils/Utils.test.ts +++ b/app/src/utils/Utils.test.ts @@ -9,6 +9,7 @@ import { getFormattedDate, getFormattedDateRangeString, getFormattedFileSize, + getFormattedIdentitySource, getLogOutUrl, isObject, jsonParseObjectProperties, @@ -17,6 +18,7 @@ import { safeJSONParse, safeJSONStringify } from './Utils'; +import { SYSTEM_IDENTITY_SOURCE } from 'hooks/useKeycloakWrapper'; describe('ensureProtocol', () => { it('upgrades the URL if string begins with `http://`', async () => { @@ -440,3 +442,41 @@ describe('pluralize', () => { expect(response).toEqual('berry'); }); }); + +describe('getFormattedIdentitySource', () => { + it('returns BCeID Basic', () => { + const result = getFormattedIdentitySource(SYSTEM_IDENTITY_SOURCE.BCEID_BASIC); + + expect(result).toEqual('BCeID Basic'); + }); + + it('returns BCeID Business', () => { + const result = getFormattedIdentitySource(SYSTEM_IDENTITY_SOURCE.BCEID_BUSINESS); + + expect(result).toEqual('BCeID Business'); + }); + + it('returns IDIR', () => { + const result = getFormattedIdentitySource(SYSTEM_IDENTITY_SOURCE.IDIR); + + expect(result).toEqual('IDIR'); + }); + + it('returns IDIR', () => { + const result = getFormattedIdentitySource(SYSTEM_IDENTITY_SOURCE.DATABASE); + + expect(result).toEqual('System'); + }); + + it('returns null for unknown identity source', () => { + const result = getFormattedIdentitySource('__default_test_string' as SYSTEM_IDENTITY_SOURCE); + + expect(result).toEqual(null); + }); + + it('returns null for null identity source', () => { + const result = getFormattedIdentitySource(null as unknown as SYSTEM_IDENTITY_SOURCE); + + expect(result).toEqual(null); + }); +}); diff --git a/app/src/utils/Utils.ts b/app/src/utils/Utils.ts index 4caf79f1e..128e2910a 100644 --- a/app/src/utils/Utils.ts +++ b/app/src/utils/Utils.ts @@ -1,6 +1,7 @@ import { DATE_FORMAT, TIME_FORMAT } from 'constants/dateTimeFormats'; import { IConfig } from 'contexts/configContext'; import { Feature, Polygon } from 'geojson'; +import { SYSTEM_IDENTITY_SOURCE } from 'hooks/useKeycloakWrapper'; import { LatLngBounds } from 'leaflet'; import _ from 'lodash'; import moment from 'moment'; @@ -349,3 +350,30 @@ export const pluralize = (quantity: number, word: string, singularSuffix = '', p export const alphabetizeObjects = (data: T[], property: string) => { return _.sortBy(data, property); }; + +/** + * Returns a human-readible identity source string. + * + * @example getFormattedIdentitySource("BCEIDBUSINESS"); // => "BCeID Business" + * + * @param {SYSTEM_IDENTITY_SOURCE} identitySource The identity source + * @returns {*} {string} the string representing the identity source + */ +export const getFormattedIdentitySource = (identitySource: SYSTEM_IDENTITY_SOURCE): string | null => { + switch (identitySource) { + case SYSTEM_IDENTITY_SOURCE.BCEID_BASIC: + return 'BCeID Basic'; + + case SYSTEM_IDENTITY_SOURCE.BCEID_BUSINESS: + return 'BCeID Business'; + + case SYSTEM_IDENTITY_SOURCE.IDIR: + return 'IDIR'; + + case SYSTEM_IDENTITY_SOURCE.DATABASE: + return 'System'; + + default: + return null; + } +};