Skip to content

Commit

Permalink
Merge pull request #59 from jeff-phillips-18/notification-drawer
Browse files Browse the repository at this point in the history
Add notifications drawer
  • Loading branch information
jeff-phillips-18 authored Jun 9, 2021
2 parents 7246ce1 + daf7c28 commit 8fd1043
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 4 deletions.
16 changes: 16 additions & 0 deletions frontend/src/app/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ 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';

const App: React.FC = () => {
const isDeskTop = useDesktopWidth();
const [isNavOpen, setIsNavOpen] = React.useState(isDeskTop);
const [notificationsOpen, setNotificationsOpen] = React.useState(false);
const dispatch = useDispatch();

useWatchBuildStatus();
Expand All @@ -34,8 +36,16 @@ const App: React.FC = () => {
return (
<Page
className="odh-dashboard"
header={<Header isNavOpen={isNavOpen} onNavToggle={onNavToggle} />}
header={
<Header
isNavOpen={isNavOpen}
onNavToggle={onNavToggle}
onNotificationsClick={() => setNotificationsOpen(!notificationsOpen)}
/>
}
sidebar={<NavSidebar isNavOpen={isNavOpen} />}
notificationDrawer={<AppNotificationDrawer onClose={() => setNotificationsOpen(false)} />}
isNotificationDrawerExpanded={notificationsOpen}
>
<Routes />
<ToastNotifications />
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/app/AppNotificationDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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<AppNotificationDrawerProps> = ({ onClose }) => {
const notifications: AppNotification[] = useSelector<State, AppNotification[]>(
(state) => state.appState.notifications,
);
const dispatch = useDispatch();
const newNotifications = React.useMemo(() => {
return notifications.filter((notification) => !notification.read).length;
}, [notifications]);
const [currentTime, setCurrentTime] = React.useState<Date>(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 (
<NotificationDrawer className="odh-dashboard__notification-drawer">
<NotificationDrawerHeader count={newNotifications} onClose={onClose} />
<NotificationDrawerBody>
{notifications.length ? (
<NotificationDrawerList>
{notifications.map((notification) => (
<NotificationDrawerListItem
key={notification.id}
variant={notification.status}
onClick={() => markNotificationRead(notification)}
isRead={notification.read}
>
<NotificationDrawerListItemHeader
variant={notification.status}
title={notification.title}
srTitle={notification.title}
>
<div>
<Button
className="odh-dashboard__notification-drawer__item-remove"
variant={ButtonVariant.plain}
aria-label="remove notification"
onClick={() => onRemoveNotification(notification)}
>
<TimesIcon aria-hidden="true" />
</Button>
</div>
</NotificationDrawerListItemHeader>
<NotificationDrawerListItemBody
timestamp={calculateRelativeTime(notification.timestamp, currentTime)}
className={notification.message ? '' : 'm-is-hidden'}
>
{notification.message}
</NotificationDrawerListItemBody>
</NotificationDrawerListItem>
))}
</NotificationDrawerList>
) : (
<EmptyState variant={EmptyStateVariant.small}>
<EmptyStateBody>There are no notifications at this time.</EmptyStateBody>
</EmptyState>
)}
</NotificationDrawerBody>
</NotificationDrawer>
);
};

export default AppNotificationDrawer;
5 changes: 3 additions & 2 deletions frontend/src/app/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import HeaderTools from './HeaderTools';
type HeaderProps = {
isNavOpen: boolean;
onNavToggle: () => void;
onNotificationsClick: () => void;
};

const Header: React.FC<HeaderProps> = ({ isNavOpen, onNavToggle }) => {
const Header: React.FC<HeaderProps> = ({ isNavOpen, onNavToggle, onNotificationsClick }) => {
return (
<PageHeader
logo={<Brand src={odhLogo} alt="ODH Logo" />}
headerTools={<HeaderTools />}
headerTools={<HeaderTools onNotificationsClick={onNotificationsClick} />}
showNavToggle
isNavOpen={isNavOpen}
onNavToggle={onNavToggle}
Expand Down
35 changes: 34 additions & 1 deletion frontend/src/app/HeaderTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Dropdown,
DropdownPosition,
DropdownToggle,
NotificationBadge,
PageHeaderTools,
PageHeaderToolsGroup,
PageHeaderToolsItem,
Expand All @@ -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<HeaderToolsProps> = ({ onNotificationsClick }) => {
const [userMenuOpen, setUserMenuOpen] = React.useState<boolean>(false);
const [helpMenuOpen, setHelpMenuOpen] = React.useState<boolean>(false);
const notifications: AppNotification[] = useSelector<State, AppNotification[]>(
(state) => state.appState.notifications,
);

const newNotifications = React.useMemo(() => {
return notifications.filter((notification) => !notification.read).length;
}, [notifications]);

const handleLogout = () => {
setUserMenuOpen(false);
Expand Down Expand Up @@ -88,6 +102,9 @@ const HeaderTools: React.FC = () => {
return (
<PageHeaderTools>
<PageHeaderToolsGroup className="hidden-xs">
<PageHeaderToolsItem>
<NotificationBadge isRead count={newNotifications} onClick={onNotificationsClick} />
</PageHeaderToolsItem>
{helpMenuItems.length > 0 ? (
<PageHeaderToolsItem>
<Dropdown
Expand All @@ -106,6 +123,22 @@ const HeaderTools: React.FC = () => {
/>
</PageHeaderToolsItem>
) : null}
<PageHeaderToolsItem>
<Dropdown
position={DropdownPosition.right}
toggle={
<DropdownToggle
id="toggle-id"
onToggle={() => setHelpMenuOpen(!helpMenuOpen)}
toggleIndicator={CaretDownIcon}
>
<QuestionCircleIcon />
</DropdownToggle>
}
isOpen={helpMenuOpen}
dropdownItems={helpMenuItems}
/>
</PageHeaderToolsItem>
<PageHeaderToolsItem>
<Dropdown
position={DropdownPosition.right}
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/utilities/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ export const getDuration = (minutes = 0): string => {
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 :/
Expand Down

0 comments on commit 8fd1043

Please sign in to comment.