-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add floating notifications for applicable events
- setup floating notification provider - applied floating notifications to all relevant instance actions - applied floating notifications to all relevant cluster actions - applied floating notifications to all relevant profile actions - applied floating notifications to all relevant network actions - applied floating notifications to all relevant storage actions - applied floating notifications to all relevant image actions - applied floating notifications to all relevant operations actions - applied floating notifications to all relevant project actions - applied floating notifications to all relevant settings actions - removed NotificationRowLegacy component - removed NotificationRow from NetworkMap, not needed - added filtering by severity for toast notifications Signed-off-by: Mason Hu <[email protected]>
- Loading branch information
Showing
89 changed files
with
1,130 additions
and
465 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
import { ICONS, Icon, Notification } from "@canonical/react-components"; | ||
import { DefaultTitles } from "@canonical/react-components/dist/components/Notification/Notification"; | ||
import { | ||
GroupedNotificationCount, | ||
ToastNotificationType, | ||
} from "context/toastNotificationProvider"; | ||
import React, { FC, useEffect, useState } from "react"; | ||
import { createPortal } from "react-dom"; | ||
import classnames from "classnames"; | ||
import { Filters, iconLookup, severityOrder } from "util/notifications"; | ||
|
||
interface Props { | ||
notifications: ToastNotificationType[]; | ||
onDismiss: (notification: ToastNotificationType) => void; | ||
onDismissAll: () => void; | ||
onDismissByFilter: ( | ||
notifications: ToastNotificationType[], | ||
filters: Filters, | ||
) => void; | ||
groupedCount: GroupedNotificationCount; | ||
} | ||
|
||
const ToastNotifications: FC<Props> = ({ | ||
notifications, | ||
onDismiss, | ||
onDismissAll, | ||
onDismissByFilter, | ||
groupedCount = {}, | ||
}) => { | ||
const [expanded, setExpanded] = useState(false); | ||
const [filters, setFilters] = useState<Filters>({}); | ||
const hasFilters = !!Object.keys(filters).length; | ||
|
||
useEffect(() => { | ||
if (notifications.length < 2) { | ||
setExpanded(false); | ||
setFilters({}); | ||
} | ||
}, [notifications.length]); | ||
|
||
const handleFilterSelect = (filter: string) => { | ||
setFilters((prev) => { | ||
const newFilters = { ...prev }; | ||
if (!newFilters[filter]) { | ||
newFilters[filter] = true; | ||
} else { | ||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete | ||
delete newFilters[filter]; | ||
} | ||
return newFilters; | ||
}); | ||
}; | ||
|
||
const handleExpansion = () => { | ||
setExpanded((prev) => !prev); | ||
if (expanded) { | ||
setFilters({}); | ||
} | ||
}; | ||
|
||
const handleDismissGroups = () => { | ||
if (hasFilters) { | ||
onDismissByFilter(notifications, filters); | ||
setFilters({}); | ||
return; | ||
} | ||
|
||
onDismissAll(); | ||
}; | ||
|
||
const getSeverityFilters = () => { | ||
const filterButtons = severityOrder.map((severity) => { | ||
if (groupedCount[severity]) { | ||
return ( | ||
<button | ||
key={`notification-filter-${severity}`} | ||
className={classnames( | ||
"u-no-margin u-no-padding u-no-border filter-button", | ||
{ "filter-button-active": filters[severity] }, | ||
)} | ||
onClick={() => handleFilterSelect(severity)} | ||
disabled={!expanded} | ||
> | ||
<Icon name={iconLookup[severity]} /> | ||
<span>{groupedCount[severity]}</span> | ||
</button> | ||
); | ||
} | ||
}); | ||
|
||
return <div className="filters">{filterButtons}</div>; | ||
}; | ||
|
||
const getDismissText = () => { | ||
if (hasFilters) { | ||
const dismissText = ( | ||
<div className="filters"> | ||
<span className="filter-text">Dismiss</span> | ||
{severityOrder.map((severity) => { | ||
if (groupedCount[severity] && filters[severity]) { | ||
return ( | ||
<span key={`dismiss-text-${severity}`} className="filter-text"> | ||
<span>{groupedCount[severity]}</span> | ||
<Icon name={iconLookup[severity]} /> | ||
</span> | ||
); | ||
} | ||
})} | ||
</div> | ||
); | ||
return dismissText; | ||
} | ||
|
||
return <span>Dismiss all {notifications.length}</span>; | ||
}; | ||
|
||
// Only filter input data if there are filters present | ||
const filteredNotifications = hasFilters | ||
? notifications.filter((notification) => filters[notification.type]) | ||
: notifications; | ||
|
||
const lastNotificationIndex = filteredNotifications.length - 1; | ||
const latestNotification = filteredNotifications[lastNotificationIndex]; | ||
// Don't assign alert role for notifications when expanded since we don't want | ||
// screen readers to announce every existing notification | ||
const notificationEls = expanded ? ( | ||
filteredNotifications.map((_, index, array) => { | ||
const lastNotificationIndex = array.length - 1; | ||
// This will map notifications in reverse order | ||
const notification = array[lastNotificationIndex - index]; | ||
return ( | ||
<Notification | ||
key={`notification-${index}`} | ||
title={notification.title ?? DefaultTitles[notification.type]} | ||
actions={notification.actions} | ||
severity={notification.type} | ||
onDismiss={() => onDismiss(notification)} | ||
className={`u-no-margin--bottom toast-notification ${index === lastNotificationIndex ? "toast-notification--no-border-bottom" : ""}`} | ||
timestamp={notification.timestamp} | ||
> | ||
{notification.message} | ||
</Notification> | ||
); | ||
}) | ||
) : ( | ||
<Notification | ||
title={latestNotification.title ?? DefaultTitles[latestNotification.type]} | ||
actions={latestNotification.actions} | ||
severity={latestNotification.type} | ||
onDismiss={() => onDismiss(latestNotification)} | ||
className="u-no-margin--bottom toast-notification" | ||
timestamp={latestNotification.timestamp} | ||
role="alert" | ||
> | ||
{latestNotification.message} | ||
</Notification> | ||
); | ||
|
||
const hasMultipleNotifications = notifications.length > 1; | ||
return ( | ||
<> | ||
{createPortal( | ||
<div className="toast-container" role="alertdialog"> | ||
{hasMultipleNotifications && ( | ||
<div className="top-summary"> | ||
{getSeverityFilters()} | ||
<button | ||
className="u-has-icon u-no-margin u-no-padding u-no-border show-hide-button" | ||
onClick={handleExpansion} | ||
> | ||
<span>{expanded ? "Hide" : "Show all"}</span> | ||
<Icon name={expanded ? ICONS.chevronDown : ICONS.chevronUp} /> | ||
</button> | ||
</div> | ||
)} | ||
{notificationEls} | ||
{hasMultipleNotifications && ( | ||
<div className="dismiss" onClick={handleDismissGroups}> | ||
<button className="u-no-margin u-no-padding u-no-border dismiss-button"> | ||
{getDismissText()} | ||
</button> | ||
</div> | ||
)} | ||
</div>, | ||
document.body, | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export default ToastNotifications; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { | ||
NotificationProvider, | ||
QueuedNotification, | ||
} from "@canonical/react-components"; | ||
import React, { FC, PropsWithChildren } from "react"; | ||
import ToastNotificationProvider from "./toastNotificationProvider"; | ||
|
||
const NewNotificationProvider: FC< | ||
PropsWithChildren<{ state: QueuedNotification["state"]; pathname?: string }> | ||
> = ({ children, state, pathname }) => { | ||
return ( | ||
<ToastNotificationProvider> | ||
<NotificationProvider state={state} pathname={pathname}> | ||
{children} | ||
</NotificationProvider> | ||
</ToastNotificationProvider> | ||
); | ||
}; | ||
|
||
export default NewNotificationProvider; |
Oops, something went wrong.