feat: define signal type for notifications

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-02-15 14:39:44 +02:00
parent 06773d802a
commit 9873c44c24
10 changed files with 130 additions and 65 deletions
@@ -45,6 +45,8 @@ export type UpdateNotificationsOptions = {
export interface NotificationsApi {
getNotifications(options?: GetNotificationsOptions): Promise<Notification[]>;
getNotification(id: string): Promise<Notification>;
getStatus(): Promise<NotificationStatus>;
updateNotifications(
@@ -74,6 +74,20 @@ describe('NotificationsClient', () => {
expect(response).toEqual(expectedResp);
});
it('should fetch single notification', async () => {
server.use(
rest.get(`${mockBaseUrl}/:id`, (req, res, ctx) => {
expect(req.params.id).toBe('acdaa8ca-262b-43c1-b74b-de06e5f3b3c7');
return res(ctx.json(testNotification));
}),
);
const response = await client.getNotification(
'acdaa8ca-262b-43c1-b74b-de06e5f3b3c7',
);
expect(response).toEqual(testNotification);
});
it('should fetch status from correct endpoint', async () => {
server.use(
rest.get(`${mockBaseUrl}/status`, (_, res, ctx) =>
@@ -60,6 +60,10 @@ export class NotificationsClient implements NotificationsApi {
return await this.request<Notification[]>(urlSegment);
}
async getNotification(id: string): Promise<Notification> {
return await this.request<Notification>(`${id}`);
}
async getStatus(): Promise<NotificationStatus> {
return await this.request<NotificationStatus>('status');
}
@@ -17,12 +17,13 @@ import React, { useEffect } from 'react';
import { useNotificationsApi } from '../../hooks';
import { SidebarItem } from '@backstage/core-components';
import NotificationsIcon from '@material-ui/icons/Notifications';
import { useRouteRef } from '@backstage/core-plugin-api';
import { 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 { useWebNotifications } from '../../hooks/useWebNotifications';
import { useTitleCounter } from '../../hooks/useTitleCounter';
import { JsonObject } from '@backstage/types';
import { notificationsApiRef } from '../../api';
/** @public */
export const NotificationsSidebarItem = (props?: {
@@ -35,11 +36,11 @@ export const NotificationsSidebarItem = (props?: {
const { loading, error, value, retry } = useNotificationsApi(api =>
api.getStatus(),
);
const notificationsApi = useApi(notificationsApiRef);
const [unreadCount, setUnreadCount] = React.useState(0);
const notificationsRoute = useRouteRef(rootRouteRef);
// TODO: Add signal type support to `useSignal` to make it a bit easier to use
// TODO: Do we want to add long polling in case signals are not available
const { lastSignal } = useSignal('notifications');
const { lastSignal } = useSignal<NotificationSignal>('notifications');
const { sendWebNotification } = useWebNotifications();
const [refresh, setRefresh] = React.useState(false);
const { setNotificationCount } = useTitleCounter();
@@ -52,39 +53,35 @@ export const NotificationsSidebarItem = (props?: {
}, [refresh, retry]);
useEffect(() => {
const handleWebNotification = (signal: JsonObject) => {
if (!webNotificationsEnabled || !('notification' in signal)) {
const handleWebNotification = (signal: NotificationSignal) => {
if (!webNotificationsEnabled || signal.action !== 'new_notification') {
return;
}
const notificationData = signal.notification as JsonObject;
if (
!notificationData ||
!('title' in notificationData) ||
!('description' in notificationData) ||
!('title' in notificationData)
) {
return;
}
const notification = sendWebNotification({
title: notificationData.title as string,
description: notificationData.description as string,
});
if (notification) {
notification.onclick = event => {
event.preventDefault();
notification.close();
window.open(notificationData.link as string, '_blank');
};
}
notificationsApi
.getNotification(signal.notification_id)
.then(notification => {
if (!notification) {
return;
}
sendWebNotification({
title: notification.payload.title,
description: notification.payload.description ?? '',
link: notification.payload.link,
});
});
};
if (lastSignal && lastSignal.action) {
handleWebNotification(lastSignal);
setRefresh(true);
}
}, [lastSignal, sendWebNotification, webNotificationsEnabled]);
}, [
lastSignal,
sendWebNotification,
webNotificationsEnabled,
notificationsApi,
]);
useEffect(() => {
if (!loading && !error && value) {
@@ -37,7 +37,7 @@ export function useWebNotifications() {
});
const sendWebNotification = useCallback(
(options: { title: string; description: string }) => {
(options: { title: string; description: string; link?: string }) => {
if (webNotificationPermission !== 'granted') {
return null;
}
@@ -45,6 +45,15 @@ export function useWebNotifications() {
const notification = new Notification(options.title, {
body: options.description,
});
notification.onclick = event => {
event.preventDefault();
notification.close();
if (options.link) {
window.open(options.link, '_blank');
}
};
return notification;
},
[webNotificationPermission],