diff --git a/.changeset/true-baboons-fold.md b/.changeset/true-baboons-fold.md new file mode 100644 index 0000000000..1233a913a3 --- /dev/null +++ b/.changeset/true-baboons-fold.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Improve customization of the notification snackbar. + +Users can now customize the notification snackbar by providing custom components and icons +for different severity levels. Additionally, the location of the snackbar notifications +can be modified, the density of the snackbar can be changed, and the number of snacks can +be limited. diff --git a/plugins/notifications/report.api.md b/plugins/notifications/report.api.md index 62c41d07c7..a9aeb64cda 100644 --- a/plugins/notifications/report.api.md +++ b/plugins/notifications/report.api.md @@ -13,6 +13,7 @@ import { Notification as Notification_2 } from '@backstage/plugin-notifications- import { NotificationSettings } from '@backstage/plugin-notifications-common'; import { NotificationSeverity } from '@backstage/plugin-notifications-common'; import { NotificationStatus } from '@backstage/plugin-notifications-common'; +import * as React_2 from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableProps } from '@backstage/core-components'; @@ -100,6 +101,23 @@ export class NotificationsClient implements NotificationsApi { ): Promise; } +// @public (undocumented) +export type NotificationSnackbarProperties = { + enabled?: boolean; + autoHideDuration?: number | null; + anchorOrigin?: { + vertical: 'top' | 'bottom'; + horizontal: 'left' | 'center' | 'right'; + }; + dense?: boolean; + maxSnack?: number; + snackStyle?: React_2.CSSProperties; + iconVariant?: Partial>; + Components?: { + [key in NotificationSeverity]: React_2.JSXElementConstructor; + }; +}; + // @public (undocumented) export const NotificationsPage: ( props?: NotificationsPageProps, @@ -125,17 +143,23 @@ export const notificationsPlugin: BackstagePlugin< >; // @public (undocumented) -export const NotificationsSidebarItem: (props?: { +export const NotificationsSidebarItem: ( + props?: NotificationsSideBarItemProps, +) => JSX_2.Element; + +// @public (undocumented) +export type NotificationsSideBarItemProps = { webNotificationsEnabled?: boolean; titleCounterEnabled?: boolean; snackbarEnabled?: boolean; snackbarAutoHideDuration?: number | null; + snackbarProps?: NotificationSnackbarProperties; className?: string; icon?: IconComponent; text?: string; disableHighlight?: boolean; noTrack?: boolean; -}) => JSX_2.Element; +}; // @public (undocumented) export const NotificationsTable: ({ diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 2a2f56da6a..c76c5d1871 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useState, useCallback, useEffect } from 'react'; +import * as React from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useNotificationsApi } from '../../hooks'; import { Link, SidebarItem } from '@backstage/core-components'; import NotificationsIcon from '@material-ui/icons/Notifications'; @@ -27,6 +28,7 @@ import { rootRouteRef } from '../../routes'; import { useSignal } from '@backstage/plugin-signals-react'; import { Notification, + NotificationSeverity, NotificationSignal, } from '@backstage/plugin-notifications-common'; import { useWebNotifications } from '../../hooks/useWebNotifications'; @@ -79,18 +81,51 @@ declare module 'notistack' { } } -/** @public */ -export const NotificationsSidebarItem = (props?: { +/** + * @public + */ +export type NotificationSnackbarProperties = { + enabled?: boolean; + autoHideDuration?: number | null; + anchorOrigin?: { + vertical: 'top' | 'bottom'; + horizontal: 'left' | 'center' | 'right'; + }; + dense?: boolean; + maxSnack?: number; + snackStyle?: React.CSSProperties; + iconVariant?: Partial>; + Components?: { + [key in NotificationSeverity]: React.JSXElementConstructor; + }; +}; + +/** + * @public + */ +export type NotificationsSideBarItemProps = { webNotificationsEnabled?: boolean; titleCounterEnabled?: boolean; + /** + * @deprecated Use `snackbarProps` instead. + */ snackbarEnabled?: boolean; + /** + * @deprecated Use `snackbarProps` instead. + */ snackbarAutoHideDuration?: number | null; + snackbarProps?: NotificationSnackbarProperties; className?: string; icon?: IconComponent; text?: string; disableHighlight?: boolean; noTrack?: boolean; -}) => { +}; + +/** @public */ +export const NotificationsSidebarItem = ( + props?: NotificationsSideBarItemProps, +) => { const { webNotificationsEnabled = false, titleCounterEnabled = true, @@ -102,10 +137,21 @@ export const NotificationsSidebarItem = (props?: { } = props ?? { webNotificationsEnabled: false, titleCounterEnabled: true, - snackbarEnabled: true, - snackbarAutoHideDuration: 10000, + snackbarProps: { + enabled: true, + autoHideDuration: 10000, + }, }; + const snackbarProps = useMemo( + () => + props?.snackbarProps ?? { + enabled: snackbarEnabled, + autoHideDuration: snackbarAutoHideDuration, + }, + [props?.snackbarProps, snackbarAutoHideDuration, snackbarEnabled], + ); + const { loading, error, value, retry } = useNotificationsApi(api => api.getStatus(), ); @@ -185,7 +231,7 @@ export const NotificationsSidebarItem = (props?: { useEffect(() => { const handleNotificationSignal = (signal: NotificationSignal) => { if ( - (!webNotificationsEnabled && !snackbarEnabled) || + (!webNotificationsEnabled && !snackbarProps.enabled) || signal.action !== 'new_notification' ) { return; @@ -204,7 +250,7 @@ export const NotificationsSidebarItem = (props?: { link: notification.payload.link, }); } - if (snackbarEnabled) { + if (snackbarProps.enabled) { const { action } = getSnackbarProperties(notification); const snackBarText = notification.payload.title.length > 50 @@ -212,10 +258,14 @@ export const NotificationsSidebarItem = (props?: { : notification.payload.title; enqueueSnackbar(snackBarText, { key: notification.id, + style: snackbarProps.snackStyle, variant: notification.payload.severity, - anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, + anchorOrigin: snackbarProps.anchorOrigin ?? { + vertical: 'bottom', + horizontal: 'right', + }, action, - autoHideDuration: snackbarAutoHideDuration, + autoHideDuration: snackbarProps.autoHideDuration, } as OptionsWithExtraProps); } }) @@ -235,11 +285,10 @@ export const NotificationsSidebarItem = (props?: { lastSignal, sendWebNotification, webNotificationsEnabled, - snackbarEnabled, - snackbarAutoHideDuration, notificationsApi, alertApi, getSnackbarProperties, + snackbarProps, ]); useEffect(() => { @@ -261,16 +310,30 @@ export const NotificationsSidebarItem = (props?: { {snackbarEnabled && ( , - critical: , - high: , - low: , + normal: snackbarProps?.iconVariant?.normal ?? ( + + ), + critical: snackbarProps?.iconVariant?.critical ?? ( + + ), + high: snackbarProps?.iconVariant?.high ?? ( + + ), + low: snackbarProps?.iconVariant?.low ?? ( + + ), }} + dense={snackbarProps?.dense} + maxSnack={snackbarProps?.maxSnack} Components={{ - normal: StyledMaterialDesignContent, - critical: StyledMaterialDesignContent, - high: StyledMaterialDesignContent, - low: StyledMaterialDesignContent, + normal: + snackbarProps?.Components?.normal ?? StyledMaterialDesignContent, + critical: + snackbarProps?.Components?.critical ?? + StyledMaterialDesignContent, + high: + snackbarProps?.Components?.high ?? StyledMaterialDesignContent, + low: snackbarProps?.Components?.low ?? StyledMaterialDesignContent, }} /> )}