feat: allow showing notifications as snackbars in the UI
Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications': patch
|
||||
---
|
||||
|
||||
Allow showing notifications as snackbars in the UI
|
||||
@@ -100,6 +100,7 @@ export const notificationsPlugin: BackstagePlugin<
|
||||
export const NotificationsSidebarItem: (props?: {
|
||||
webNotificationsEnabled?: boolean;
|
||||
titleCounterEnabled?: boolean;
|
||||
snackbarEnabled?: boolean;
|
||||
className?: string;
|
||||
icon?: IconComponent;
|
||||
text?: string;
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+146
-19
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user