From daf7c28aed04306a64a78c44e9a0e6369d38fcf3 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Date: Mon, 7 Jun 2021 07:27:02 -0400 Subject: [PATCH] Add notifications drawer --- frontend/src/app/App.scss | 16 ++++ frontend/src/app/App.tsx | 12 ++- frontend/src/app/AppNotificationDrawer.tsx | 99 ++++++++++++++++++++++ frontend/src/app/Header.tsx | 5 +- frontend/src/app/HeaderTools.tsx | 35 +++++++- frontend/src/utilities/utils.ts | 25 ++++++ 6 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 frontend/src/app/AppNotificationDrawer.tsx diff --git a/frontend/src/app/App.scss b/frontend/src/app/App.scss index 35f5ca2056..895e976824 100644 --- a/frontend/src/app/App.scss +++ b/frontend/src/app/App.scss @@ -18,6 +18,19 @@ html, body, #root { } } + &__notification-drawer { + .pf-c-notification-drawer__list-item-header-title { + font-size: 1em; + } + &__item-remove { + padding-bottom: 0; + padding-top: 0; + } + .pf-c-notification-drawer__list-item-description.m-is-hidden { + display: none; + } + } + &__notifications { position: fixed; right: var(--pf-global--spacer--sm); @@ -77,6 +90,9 @@ html, body, #root { } } } + .pf-c-drawer.pf-m-inline { + flex: 1; + } // Bootstrap Overrides (Bootstrap pulled in by @cloudmosaic/quickstarts) code { color: initial; diff --git a/frontend/src/app/App.tsx b/frontend/src/app/App.tsx index d2f5f99096..423de6807d 100644 --- a/frontend/src/app/App.tsx +++ b/frontend/src/app/App.tsx @@ -8,6 +8,7 @@ import Header from './Header'; import Routes from './Routes'; import NavSidebar from './NavSidebar'; import ToastNotifications from '../components/ToastNotifications'; +import AppNotificationDrawer from './AppNotificationDrawer'; import { useWatchBuildStatus } from '../utilities/useWatchBuildStatus'; import './App.scss'; @@ -15,6 +16,7 @@ import './App.scss'; const App: React.FC = () => { const isDeskTop = useDesktopWidth(); const [isNavOpen, setIsNavOpen] = React.useState(isDeskTop); + const [notificationsOpen, setNotificationsOpen] = React.useState(false); const dispatch = useDispatch(); useWatchBuildStatus(); @@ -34,8 +36,16 @@ const App: React.FC = () => { return ( } + header={ +
setNotificationsOpen(!notificationsOpen)} + /> + } sidebar={} + notificationDrawer={ setNotificationsOpen(false)} />} + isNotificationDrawerExpanded={notificationsOpen} > diff --git a/frontend/src/app/AppNotificationDrawer.tsx b/frontend/src/app/AppNotificationDrawer.tsx new file mode 100644 index 0000000000..4502724ad1 --- /dev/null +++ b/frontend/src/app/AppNotificationDrawer.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Button, + ButtonVariant, + NotificationDrawer, + NotificationDrawerHeader, + NotificationDrawerBody, + NotificationDrawerList, + NotificationDrawerListItem, + NotificationDrawerListItemHeader, + NotificationDrawerListItemBody, + EmptyStateVariant, + EmptyState, + EmptyStateBody, +} from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons'; +import { AppNotification, State } from '../redux/types'; +import { ackNotification, removeNotification } from '../redux/actions/actions'; +import { calculateRelativeTime } from '../utilities/utils'; + +interface AppNotificationDrawerProps { + onClose: () => void; +} + +const AppNotificationDrawer: React.FC = ({ onClose }) => { + const notifications: AppNotification[] = useSelector( + (state) => state.appState.notifications, + ); + const dispatch = useDispatch(); + const newNotifications = React.useMemo(() => { + return notifications.filter((notification) => !notification.read).length; + }, [notifications]); + const [currentTime, setCurrentTime] = React.useState(new Date()); + + React.useEffect(() => { + const timeHandle = setInterval(() => setCurrentTime(new Date()), 20 * 1000); + return () => { + clearInterval(timeHandle); + }; + }, []); + + const markNotificationRead = (notification: AppNotification): void => { + dispatch(ackNotification(notification)); + }; + + const onRemoveNotification = (notification: AppNotification): void => { + dispatch(removeNotification(notification)); + }; + + return ( + + + + {notifications.length ? ( + + {notifications.map((notification) => ( + markNotificationRead(notification)} + isRead={notification.read} + > + +
+ +
+
+ + {notification.message} + +
+ ))} +
+ ) : ( + + There are no notifications at this time. + + )} +
+
+ ); +}; + +export default AppNotificationDrawer; diff --git a/frontend/src/app/Header.tsx b/frontend/src/app/Header.tsx index fdeff32f09..8cb21e0c7e 100644 --- a/frontend/src/app/Header.tsx +++ b/frontend/src/app/Header.tsx @@ -6,13 +6,14 @@ import HeaderTools from './HeaderTools'; type HeaderProps = { isNavOpen: boolean; onNavToggle: () => void; + onNotificationsClick: () => void; }; -const Header: React.FC = ({ isNavOpen, onNavToggle }) => { +const Header: React.FC = ({ isNavOpen, onNavToggle, onNotificationsClick }) => { return ( } - headerTools={} + headerTools={} showNavToggle isNavOpen={isNavOpen} onNavToggle={onNavToggle} diff --git a/frontend/src/app/HeaderTools.tsx b/frontend/src/app/HeaderTools.tsx index 98f5f09af1..7ff3eebe0d 100644 --- a/frontend/src/app/HeaderTools.tsx +++ b/frontend/src/app/HeaderTools.tsx @@ -3,6 +3,7 @@ import { Dropdown, DropdownPosition, DropdownToggle, + NotificationBadge, PageHeaderTools, PageHeaderToolsGroup, PageHeaderToolsItem, @@ -15,10 +16,23 @@ import { UserIcon, } from '@patternfly/react-icons'; import { COMMUNITY_LINK, DOC_LINK, SUPPORT_LINK } from '../utilities/const'; +import { AppNotification, State } from '../redux/types'; +import { useSelector } from 'react-redux'; -const HeaderTools: React.FC = () => { +interface HeaderToolsProps { + onNotificationsClick: () => void; +} + +const HeaderTools: React.FC = ({ onNotificationsClick }) => { const [userMenuOpen, setUserMenuOpen] = React.useState(false); const [helpMenuOpen, setHelpMenuOpen] = React.useState(false); + const notifications: AppNotification[] = useSelector( + (state) => state.appState.notifications, + ); + + const newNotifications = React.useMemo(() => { + return notifications.filter((notification) => !notification.read).length; + }, [notifications]); const handleLogout = () => { setUserMenuOpen(false); @@ -88,6 +102,9 @@ const HeaderTools: React.FC = () => { return ( + + + {helpMenuItems.length > 0 ? ( { /> ) : null} + + setHelpMenuOpen(!helpMenuOpen)} + toggleIndicator={CaretDownIcon} + > + + + } + isOpen={helpMenuOpen} + dropdownItems={helpMenuItems} + /> + { return mins > 0 ? `${mins} ${mins > 1 ? 'minutes' : 'minute'}` : ''; }; +export const calculateRelativeTime = (startTime: Date, endTime: Date): string => { + const start = startTime.getTime(); + const end = endTime.getTime(); + + const secondsAgo = (end - start) / 1000; + const minutesAgo = secondsAgo / 60; + const hoursAgo = minutesAgo / 60; + + if (minutesAgo > 90) { + const count = Math.round(hoursAgo); + return `about ${count} hours ago`; + } + if (minutesAgo > 45) { + return 'about an hour ago'; + } + if (secondsAgo > 90) { + const count = Math.round(minutesAgo); + return `about ${count} minutes ago`; + } + if (secondsAgo > 45) { + return 'about a minute ago'; + } + return 'a few seconds ago'; +}; + // Returns the possible colors allowed for a patternly-react Label component // There is no type defined for this so it must be exactly one of the possible strings // required :/