feat: allow showing notifications as snackbars in the UI

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-04-21 19:39:03 +03:00
parent 41d5c56e18
commit bfcb2f1281
5 changed files with 177 additions and 20 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications': patch
---
Allow showing notifications as snackbars in the UI
+1
View File
@@ -100,6 +100,7 @@ export const notificationsPlugin: BackstagePlugin<
export const NotificationsSidebarItem: (props?: {
webNotificationsEnabled?: boolean;
titleCounterEnabled?: boolean;
snackbarEnabled?: boolean;
className?: string;
icon?: IconComponent;
text?: string;
+1
View File
@@ -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"
},
@@ -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) => (
<>
<IconButton
onClick={() => {
if (notification.payload.link) {
window.open(notification.payload.link, '_blank');
}
navigate(notificationsRoute());
closeSnackbar(snackBarId);
}}
>
<OpenInNew fontSize="small" />
</IconButton>
<IconButton
onClick={() => {
notificationsApi.updateNotifications({
ids: [notification.id],
read: true,
});
closeSnackbar(snackBarId);
}}
>
<MarkAsReadIcon fontSize="small" />
</IconButton>
</>
);
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<VariantType>);
}
});
};
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 (
<SidebarItem
to={notificationsRoute()}
hasNotifications={!error && !!unreadCount}
text={text}
icon={icon}
{...restProps}
/>
<>
{snackbarEnabled && (
<SnackbarProvider
iconVariant={{
normal: <SeverityIcon severity="normal" />,
critical: <SeverityIcon severity="critical" />,
high: <SeverityIcon severity="high" />,
low: <SeverityIcon severity="low" />,
}}
Components={{
normal: StyledMaterialDesignContent,
critical: StyledMaterialDesignContent,
high: StyledMaterialDesignContent,
low: StyledMaterialDesignContent,
}}
/>
)}
<SidebarItem
to={notificationsRoute()}
hasNotifications={!error && !!unreadCount}
text={text}
icon={icon}
{...restProps}
/>
</>
);
};
+24 -1
View File
@@ -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"