232 lines
7.7 KiB
JavaScript
232 lines
7.7 KiB
JavaScript
import { useQuery, gql } from '@apollo/client'
|
|
import ClickAwayListener from '@mui/material/ClickAwayListener'
|
|
import Popper from '@mui/material/Popper'
|
|
import classnames from 'classnames'
|
|
import * as R from 'ramda'
|
|
import React, { memo, useState, useEffect, useRef } from 'react'
|
|
import { Link as WLink, useRoute, useLocation } from 'wouter'
|
|
import ActionButton from '../buttons/ActionButton'
|
|
import { H4 } from '../typography'
|
|
import AddIconReverse from '../../styling/icons/button/add/white.svg?react'
|
|
import AddIcon from '../../styling/icons/button/add/zodiac.svg?react'
|
|
import Logo from '../../styling/icons/menu/logo.svg?react'
|
|
import NotificationIcon from '../../styling/icons/menu/notification.svg?react'
|
|
|
|
import NotificationCenter from '../NotificationCenter'
|
|
import AddMachine from '../../pages/AddMachine'
|
|
|
|
import styles from './Header.module.css'
|
|
|
|
const HAS_UNREAD = gql`
|
|
query getUnread {
|
|
hasUnreadNotifications
|
|
}
|
|
`
|
|
|
|
const Link = ({
|
|
setActive,
|
|
isParent,
|
|
className,
|
|
activeClassName,
|
|
item,
|
|
...props
|
|
}) => {
|
|
const [location] = useLocation()
|
|
const [isActive] = useRoute(props.to)
|
|
if (isActive) setActive(item)
|
|
|
|
const isParentActive = isParent && location.startsWith(props.to)
|
|
|
|
const classNames = classnames({
|
|
[className]: true,
|
|
[activeClassName]: isActive || isParentActive,
|
|
})
|
|
|
|
return (
|
|
<WLink {...props} asChild>
|
|
<a className={classNames}>{props.children}</a>
|
|
</WLink>
|
|
)
|
|
}
|
|
|
|
const Subheader = ({ item, user }) => {
|
|
const [prev, setPrev] = useState(null)
|
|
|
|
return (
|
|
<div className={styles.subheader}>
|
|
<div className={styles.content}>
|
|
<nav>
|
|
<ul className={styles.subheaderUl}>
|
|
{item.children.map((it, idx) => {
|
|
if (!R.includes(user.role, it.allowedRoles)) return <></>
|
|
return (
|
|
<li key={idx} className={styles.subheaderLi}>
|
|
<Link
|
|
to={it.route}
|
|
state={{ prev }}
|
|
className={styles.subheaderLink}
|
|
activeClassName={styles.activeSubheaderLink}
|
|
item={it.route}
|
|
setActive={setPrev}>
|
|
{it.label}
|
|
</Link>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const notNil = R.compose(R.not, R.isNil)
|
|
|
|
const Header = memo(({ tree, user }) => {
|
|
const [open, setOpen] = useState(false)
|
|
const [anchorEl, setAnchorEl] = useState(null)
|
|
const [notifButtonCoords, setNotifButtonCoords] = useState({ x: 0, y: 0 })
|
|
const [active, setActive] = useState()
|
|
const [hasUnread, setHasUnread] = useState(false)
|
|
|
|
const { data, refetch, startPolling, stopPolling } = useQuery(HAS_UNREAD)
|
|
const notifCenterButtonRef = useRef()
|
|
const popperRef = useRef()
|
|
const [, navigate] = useLocation()
|
|
|
|
useEffect(() => {
|
|
if (data?.hasUnreadNotifications) return setHasUnread(true)
|
|
// if not true, make sure it's false and not undefined
|
|
if (notNil(data?.hasUnreadNotifications)) return setHasUnread(false)
|
|
}, [data])
|
|
|
|
useEffect(() => {
|
|
startPolling(60000)
|
|
return stopPolling
|
|
})
|
|
|
|
const onPaired = machine => {
|
|
setOpen(false)
|
|
navigate('/maintenance/machine-status', { state: { id: machine.deviceId } })
|
|
}
|
|
|
|
// these inline styles prevent scroll bubbling: when the user reaches the bottom of the notifications list and keeps scrolling,
|
|
// the body scrolls, stealing the focus from the notification center, preventing the admin from scrolling the notifications back up
|
|
// on the first scroll, needing to move the mouse to recapture the focus on the notification center
|
|
// it also disables the scrollbars caused by the notification center's background to the right of the page, but keeps the scrolling on the body enabled
|
|
const onClickAway = () => {
|
|
setAnchorEl(null)
|
|
document.querySelector('#root').classList.remove('root-notifcenter-open')
|
|
document.querySelector('body').classList.remove('body-notifcenter-open')
|
|
}
|
|
|
|
const handleClick = event => {
|
|
const coords = notifCenterButtonRef.current.getBoundingClientRect()
|
|
setNotifButtonCoords({ x: coords.x, y: coords.y + 5 })
|
|
|
|
setAnchorEl(anchorEl ? null : event.currentTarget)
|
|
document.querySelector('#root').classList.add('root-notifcenter-open')
|
|
document.querySelector('body').classList.add('body-notifcenter-open')
|
|
}
|
|
|
|
const popperOpen = Boolean(anchorEl)
|
|
const id = popperOpen ? 'notifications-popper' : undefined
|
|
return (
|
|
<header className={styles.headerContainer}>
|
|
<div className={styles.header}>
|
|
<div className={styles.content}>
|
|
<div
|
|
onClick={() => {
|
|
setActive(false)
|
|
navigate('/dashboard')
|
|
}}
|
|
className={classnames(styles.logo, styles.logoLink)}>
|
|
<Logo />
|
|
<H4 className="text-white">Lamassu Admin</H4>
|
|
</div>
|
|
<nav className={styles.nav}>
|
|
<ul className={styles.ul}>
|
|
{tree.map((it, idx) => {
|
|
if (!R.includes(user.role, it.allowedRoles)) return <></>
|
|
return (
|
|
<Link
|
|
isParent
|
|
key={idx}
|
|
to={it.route || it.children[0].route}
|
|
setActive={setActive}
|
|
item={it}
|
|
className={styles.link}
|
|
activeClassName={styles.activeLink}>
|
|
<li className={styles.li}>
|
|
<span
|
|
className={styles.forceSize}
|
|
data-forcesize={it.label}>
|
|
{it.label}
|
|
</span>
|
|
</li>
|
|
</Link>
|
|
)
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
<div className={styles.actionButtonsContainer}>
|
|
<ActionButton
|
|
altTextColor
|
|
color="secondary"
|
|
Icon={AddIcon}
|
|
InverseIcon={AddIconReverse}
|
|
onClick={() => setOpen(true)}>
|
|
Add machine
|
|
</ActionButton>
|
|
<ClickAwayListener onClickAway={onClickAway}>
|
|
<div ref={notifCenterButtonRef}>
|
|
<button
|
|
onClick={handleClick}
|
|
className={styles.notificationIcon}>
|
|
<NotificationIcon />
|
|
{hasUnread && <div className={styles.hasUnread} />}
|
|
</button>
|
|
<Popper
|
|
ref={popperRef}
|
|
id={id}
|
|
open={popperOpen}
|
|
anchorEl={anchorEl}
|
|
className={styles.popper}
|
|
disablePortal={false}
|
|
placement="bottom-end"
|
|
modifiers={[
|
|
{
|
|
name: 'offset',
|
|
enabled: true,
|
|
options: {
|
|
offset: ['100vw', '100vw'],
|
|
},
|
|
},
|
|
{
|
|
name: 'preventOverflow',
|
|
enabled: true,
|
|
options: {
|
|
rootBoundary: 'viewport',
|
|
},
|
|
},
|
|
]}>
|
|
<NotificationCenter
|
|
popperRef={popperRef}
|
|
buttonCoords={notifButtonCoords}
|
|
close={onClickAway}
|
|
hasUnreadProp={hasUnread}
|
|
refetchHasUnreadHeader={refetch}
|
|
/>
|
|
</Popper>
|
|
</div>
|
|
</ClickAwayListener>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{active && active.children && <Subheader item={active} user={user} />}
|
|
{open && <AddMachine close={() => setOpen(false)} onPaired={onPaired} />}
|
|
</header>
|
|
)
|
|
})
|
|
|
|
export default Header
|