diff --git a/.changeset/hot-forks-train.md b/.changeset/hot-forks-train.md
new file mode 100644
index 0000000000..a5300085b6
--- /dev/null
+++ b/.changeset/hot-forks-train.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-notifications': patch
+---
+
+Allow showing notifications as snackbars in the UI
diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md
index e998d76ea4..9ffcf0d727 100644
--- a/plugins/notifications/api-report.md
+++ b/plugins/notifications/api-report.md
@@ -100,6 +100,7 @@ export const notificationsPlugin: BackstagePlugin<
export const NotificationsSidebarItem: (props?: {
webNotificationsEnabled?: boolean;
titleCounterEnabled?: boolean;
+ snackbarEnabled?: boolean;
className?: string;
icon?: IconComponent;
text?: string;
diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json
index 8c3a90c092..0c724e3d02 100644
--- a/plugins/notifications/package.json
+++ b/plugins/notifications/package.json
@@ -43,6 +43,7 @@
"@material-ui/lab": "^4.0.0-alpha.61",
"@types/react": "^16.13.1 || ^17.0.0",
"lodash": "^4.17.21",
+ "notistack": "^3.0.1",
"react-relative-time": "^0.0.9",
"react-use": "^17.2.4"
},
diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx
index 6daea5ee34..9851ff8a66 100644
--- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx
+++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx
@@ -13,22 +13,72 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React, { useEffect } from 'react';
+import React, { useCallback, useEffect } from 'react';
import { useNotificationsApi } from '../../hooks';
import { SidebarItem } from '@backstage/core-components';
import NotificationsIcon from '@material-ui/icons/Notifications';
import { IconComponent, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { rootRouteRef } from '../../routes';
import { useSignal } from '@backstage/plugin-signals-react';
-import { NotificationSignal } from '@backstage/plugin-notifications-common';
+import {
+ Notification,
+ NotificationSignal,
+} from '@backstage/plugin-notifications-common';
import { useWebNotifications } from '../../hooks/useWebNotifications';
import { useTitleCounter } from '../../hooks/useTitleCounter';
import { notificationsApiRef } from '../../api';
+import {
+ closeSnackbar,
+ enqueueSnackbar,
+ MaterialDesignContent,
+ OptionsWithExtraProps,
+ SnackbarKey,
+ SnackbarProvider,
+ VariantType,
+} from 'notistack';
+import { SeverityIcon } from '../NotificationsTable/SeverityIcon';
+import { useNavigate } from 'react-router-dom';
+import OpenInNew from '@material-ui/icons/OpenInNew';
+import MarkAsReadIcon from '@material-ui/icons/CheckCircle';
+import IconButton from '@material-ui/core/IconButton';
+import { styled } from '@material-ui/core/styles';
+
+const StyledMaterialDesignContent = styled(MaterialDesignContent)(
+ ({ theme }) => ({
+ '&.notistack-MuiContent-low': {
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+ },
+ '&.notistack-MuiContent-normal': {
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+ },
+ '&.notistack-MuiContent-high': {
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+ },
+ '&.notistack-MuiContent-critical': {
+ backgroundColor: theme.palette.background.default,
+ color: theme.palette.text.primary,
+ },
+ }),
+);
+
+declare module 'notistack' {
+ interface VariantOverrides {
+ // Custom variants for the snackbar
+ low: true;
+ normal: true;
+ high: true;
+ critical: true;
+ }
+}
/** @public */
export const NotificationsSidebarItem = (props?: {
webNotificationsEnabled?: boolean;
titleCounterEnabled?: boolean;
+ snackbarEnabled?: boolean;
className?: string;
icon?: IconComponent;
text?: string;
@@ -38,14 +88,20 @@ export const NotificationsSidebarItem = (props?: {
const {
webNotificationsEnabled = false,
titleCounterEnabled = true,
+ snackbarEnabled = true,
icon = NotificationsIcon,
text = 'Notifications',
...restProps
- } = props ?? { webNotificationsEnabled: false, titleCounterEnabled: true };
+ } = props ?? {
+ webNotificationsEnabled: false,
+ titleCounterEnabled: true,
+ snackbarEnabled: true,
+ };
const { loading, error, value, retry } = useNotificationsApi(api =>
api.getStatus(),
);
+ const navigate = useNavigate();
const notificationsApi = useApi(notificationsApiRef);
const [unreadCount, setUnreadCount] = React.useState(0);
const notificationsRoute = useRouteRef(rootRouteRef);
@@ -55,6 +111,40 @@ export const NotificationsSidebarItem = (props?: {
const [refresh, setRefresh] = React.useState(false);
const { setNotificationCount } = useTitleCounter();
+ const getSnackbarProperties = useCallback(
+ (notification: Notification) => {
+ const action = (snackBarId: SnackbarKey) => (
+ <>
+ {
+ if (notification.payload.link) {
+ window.open(notification.payload.link, '_blank');
+ }
+ navigate(notificationsRoute());
+ closeSnackbar(snackBarId);
+ }}
+ >
+
+
+ {
+ notificationsApi.updateNotifications({
+ ids: [notification.id],
+ read: true,
+ });
+ closeSnackbar(snackBarId);
+ }}
+ >
+
+
+ >
+ );
+
+ return { action };
+ },
+ [notificationsRoute, navigate, notificationsApi],
+ );
+
useEffect(() => {
if (refresh) {
retry();
@@ -63,8 +153,11 @@ export const NotificationsSidebarItem = (props?: {
}, [refresh, retry]);
useEffect(() => {
- const handleWebNotification = (signal: NotificationSignal) => {
- if (!webNotificationsEnabled || signal.action !== 'new_notification') {
+ const handleNotificationSignal = (signal: NotificationSignal) => {
+ if (
+ (!webNotificationsEnabled && !snackbarEnabled) ||
+ signal.action !== 'new_notification'
+ ) {
return;
}
@@ -74,24 +167,40 @@ export const NotificationsSidebarItem = (props?: {
if (!notification) {
return;
}
- sendWebNotification({
- id: notification.id,
- title: notification.payload.title,
- description: notification.payload.description ?? '',
- link: notification.payload.link,
- });
+ if (webNotificationsEnabled) {
+ sendWebNotification({
+ id: notification.id,
+ title: notification.payload.title,
+ description: notification.payload.description ?? '',
+ link: notification.payload.link,
+ });
+ }
+ if (snackbarEnabled) {
+ const { action } = getSnackbarProperties(notification);
+ const snackBarText =
+ notification.payload.title.length > 50
+ ? `${notification.payload.title.substring(0, 50)}...`
+ : notification.payload.title;
+ enqueueSnackbar(snackBarText, {
+ variant: notification.payload.severity,
+ anchorOrigin: { vertical: 'bottom', horizontal: 'right' },
+ action,
+ } as OptionsWithExtraProps);
+ }
});
};
if (lastSignal && lastSignal.action) {
- handleWebNotification(lastSignal);
+ handleNotificationSignal(lastSignal);
setRefresh(true);
}
}, [
lastSignal,
sendWebNotification,
webNotificationsEnabled,
+ snackbarEnabled,
notificationsApi,
+ getSnackbarProperties,
]);
useEffect(() => {
@@ -108,12 +217,30 @@ export const NotificationsSidebarItem = (props?: {
// TODO: Figure out if the count can be added to hasNotifications
return (
-
+ <>
+ {snackbarEnabled && (
+ ,
+ critical: ,
+ high: ,
+ low: ,
+ }}
+ Components={{
+ normal: StyledMaterialDesignContent,
+ critical: StyledMaterialDesignContent,
+ high: StyledMaterialDesignContent,
+ low: StyledMaterialDesignContent,
+ }}
+ />
+ )}
+
+ >
);
};
diff --git a/yarn.lock b/yarn.lock
index a07e9249f7..0ee0b47946 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6071,6 +6071,7 @@ __metadata:
"@types/react": ^16.13.1 || ^17.0.0
lodash: ^4.17.21
msw: ^1.0.0
+ notistack: ^3.0.1
react-relative-time: ^0.0.9
react-use: ^17.2.4
peerDependencies:
@@ -20124,7 +20125,7 @@ __metadata:
languageName: node
linkType: hard
-"clsx@npm:^1.0.2, clsx@npm:^1.0.4, clsx@npm:^1.1.1, clsx@npm:^1.2.1":
+"clsx@npm:^1.0.2, clsx@npm:^1.0.4, clsx@npm:^1.1.0, clsx@npm:^1.1.1, clsx@npm:^1.2.1":
version: 1.2.1
resolution: "clsx@npm:1.2.1"
checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12
@@ -25650,6 +25651,15 @@ __metadata:
languageName: node
linkType: hard
+"goober@npm:^2.0.33":
+ version: 2.1.14
+ resolution: "goober@npm:2.1.14"
+ peerDependencies:
+ csstype: ^3.0.10
+ checksum: 78978b7192d6a1af5cfbf1fd64b661b5f53ee6c733554b1f1b2ad3e1e2c979847fc080434390647640bb8358c0b193895d0007432c0886d12001f02f8f56b5e6
+ languageName: node
+ linkType: hard
+
"google-auth-library@npm:^9.0.0, google-auth-library@npm:^9.6.3":
version: 9.8.0
resolution: "google-auth-library@npm:9.8.0"
@@ -32361,6 +32371,19 @@ __metadata:
languageName: node
linkType: hard
+"notistack@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "notistack@npm:3.0.1"
+ dependencies:
+ clsx: ^1.1.0
+ goober: ^2.0.33
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ checksum: 421c970308690b8c8cb2e275e7f66020db7c1955e104f638e7fa562396a6b9322ff95f0e62492b07f3d36b0ef72adb4de2c2ce9803089c1c8f028d1a3b088e01
+ languageName: node
+ linkType: hard
+
"npm-bundled@npm:^1.1.1":
version: 1.1.2
resolution: "npm-bundled@npm:1.1.2"