Merge pull request #30897 from drodil/notification_snackbar_customization

feat: improve customization of notification snackbar
This commit is contained in:
Fredrik Adelöw
2025-08-19 09:13:51 +02:00
committed by GitHub
3 changed files with 119 additions and 22 deletions
+10
View File
@@ -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.
+26 -2
View File
@@ -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<NotificationSettings>;
}
// @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<Record<NotificationSeverity, React_2.ReactNode>>;
Components?: {
[key in NotificationSeverity]: React_2.JSXElementConstructor<any>;
};
};
// @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: ({
@@ -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<Record<NotificationSeverity, React.ReactNode>>;
Components?: {
[key in NotificationSeverity]: React.JSXElementConstructor<any>;
};
};
/**
* @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<VariantType>);
}
})
@@ -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 && (
<SnackbarProvider
iconVariant={{
normal: <SeverityIcon severity="normal" />,
critical: <SeverityIcon severity="critical" />,
high: <SeverityIcon severity="high" />,
low: <SeverityIcon severity="low" />,
normal: snackbarProps?.iconVariant?.normal ?? (
<SeverityIcon severity="normal" />
),
critical: snackbarProps?.iconVariant?.critical ?? (
<SeverityIcon severity="critical" />
),
high: snackbarProps?.iconVariant?.high ?? (
<SeverityIcon severity="high" />
),
low: snackbarProps?.iconVariant?.low ?? (
<SeverityIcon severity="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,
}}
/>
)}