Skip to content

Commit bc98491

Browse files
authored
fix: make the notifications icon active in dynamic plugin (#1111)
FLPATH-905: make the notifications icon active in dynamic plugin
1 parent 24d477c commit bc98491

File tree

10 files changed

+165
-145
lines changed

10 files changed

+165
-145
lines changed

plugins/notifications/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
3939
...
4040
{/* New code: */}
4141
<SidebarDivider />
42-
<NotificationsSidebarItem pollingInterval={5000} />
42+
<SidebarItem icon={NotificationsActiveIcon} to="notifications" text="Notifications" />
4343
4444
{/* Existing code for reference: */}
4545
<SidebarSpace />
@@ -55,13 +55,13 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
5555
In the `packages/app/src/App.tsx`:
5656

5757
```
58-
import { NOTIFICATIONS_ROUTE, NotificationsPage } from '@janus-idp/plugin-notifications';
58+
import { NotificationsPage } from '@janus-idp/plugin-notifications';
5959
...
6060
6161
export const AppBase = () => {
6262
...
6363
{/* New code: */}
64-
<Route path={NOTIFICATIONS_ROUTE} element={<NotificationsPage />} />
64+
<Route path="/notifications" element={<NotificationsPage />} />
6565
```
6666

6767
## How to use the NotificationApi

plugins/notifications/app-config.janus-idp.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ dynamicPlugins:
44
appIcons:
55
- name: notificationsIcon
66
module: NotificationsPlugin
7-
importName: NotificationsIcon
7+
importName: NotificationsActiveIcon
88
dynamicRoutes:
99
- path: /notifications
1010
importName: NotificationsPage
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React from 'react';
2+
3+
import { configApiRef, useApi } from '@backstage/core-plugin-api';
4+
5+
import { Badge, Tooltip } from '@material-ui/core';
6+
import NotificationsIcon from '@material-ui/icons/Notifications';
7+
import NotificationsOffIcon from '@material-ui/icons/NotificationsOff';
8+
9+
import { notificationsApiRef } from '../../api';
10+
import { DefaultPollingIntervalMs } from '../../constants';
11+
import { Notification } from '../../openapi';
12+
import { usePollingEffect } from '../usePollingEffect';
13+
import { SystemNotificationAlert } from './SystemNotificationAlert';
14+
15+
const NotificationsErrorIcon = () => (
16+
<Tooltip title="Failed to load notifications">
17+
<NotificationsOffIcon />
18+
</Tooltip>
19+
);
20+
21+
/**
22+
* Dynamic plugins recently do not support passing configuration
23+
* to icons or making the left-side menu item texts active (so far strings only).
24+
*
25+
* This Icon component tries to workaround these limitations but will be subject of
26+
* change as the extension points by dynamic plugins will evolve.
27+
*/
28+
export const NotificationsActiveIcon = () => {
29+
const notificationsApi = useApi(notificationsApiRef);
30+
const configApi = useApi(configApiRef);
31+
32+
let pollingInterval = configApi.getOptionalNumber(
33+
'notifications.pollingIntervalMs',
34+
);
35+
if (pollingInterval === undefined) {
36+
pollingInterval = DefaultPollingIntervalMs;
37+
}
38+
39+
const [error, setError] = React.useState<Error | undefined>(undefined);
40+
const [unreadCount, setUnreadCount] = React.useState(0);
41+
const [pageLoadingTime] = React.useState(new Date(Date.now()));
42+
const [lastSystemWideNotification, setLastSystemWideNotification] =
43+
React.useState<Notification>();
44+
const [closedNotificationId, setClosedNotificationId] =
45+
React.useState<string>();
46+
47+
const pollCallback = React.useCallback(async () => {
48+
try {
49+
setUnreadCount(
50+
await notificationsApi.getNotificationsCount({
51+
read: false,
52+
messageScope: 'user',
53+
}),
54+
);
55+
56+
const data = await notificationsApi.getNotifications({
57+
pageSize: 1,
58+
pageNumber: 1,
59+
createdAfter: pageLoadingTime,
60+
orderBy: 'created',
61+
orderByDirec: 'desc',
62+
messageScope: 'system',
63+
});
64+
65+
setLastSystemWideNotification(data?.[0]);
66+
} catch (e: unknown) {
67+
setError(e as Error);
68+
}
69+
}, [notificationsApi, pageLoadingTime]);
70+
71+
usePollingEffect(pollCallback, [], pollingInterval);
72+
73+
if (!!error) {
74+
return <NotificationsErrorIcon />;
75+
}
76+
77+
if (unreadCount) {
78+
return (
79+
<>
80+
<Badge color="secondary" variant="dot" overlap="circular">
81+
<NotificationsIcon />
82+
</Badge>
83+
84+
{lastSystemWideNotification &&
85+
!lastSystemWideNotification.readByUser &&
86+
closedNotificationId !== lastSystemWideNotification.id && (
87+
<SystemNotificationAlert
88+
message={lastSystemWideNotification.title}
89+
onCloseNotification={() =>
90+
setClosedNotificationId(lastSystemWideNotification.id)
91+
}
92+
/>
93+
)}
94+
</>
95+
);
96+
}
97+
98+
return <NotificationsIcon />;
99+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
3+
import { useRouteRef } from '@backstage/core-plugin-api';
4+
5+
import { IconButton, Link, makeStyles, Snackbar } from '@material-ui/core';
6+
import CloseIcon from '@material-ui/icons/Close';
7+
8+
import { notificationsRootRouteRef } from '../../routes';
9+
10+
const useStyles = makeStyles(_theme => ({
11+
systemAlertAction: {
12+
marginRight: '1rem',
13+
},
14+
}));
15+
16+
export type SystemNotificationAlertProps = {
17+
message: string;
18+
onCloseNotification: () => void;
19+
};
20+
21+
export const SystemNotificationAlert = ({
22+
message,
23+
onCloseNotification,
24+
}: SystemNotificationAlertProps) => {
25+
const styles = useStyles();
26+
const notificationsRoute = useRouteRef(notificationsRootRouteRef);
27+
28+
return (
29+
<Snackbar
30+
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
31+
open
32+
message={message}
33+
action={
34+
<>
35+
<Link
36+
href={`${notificationsRoute()}/updates`}
37+
className={styles.systemAlertAction}
38+
>
39+
Show
40+
</Link>
41+
<IconButton
42+
size="small"
43+
aria-label="close"
44+
color="inherit"
45+
onClick={onCloseNotification}
46+
>
47+
<CloseIcon fontSize="small" />
48+
</IconButton>
49+
</>
50+
}
51+
/>
52+
);
53+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './NotificationsActiveIcon';

plugins/notifications/src/components/NotificationsSidebarItem.tsx

Lines changed: 0 additions & 128 deletions
This file was deleted.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
export const NOTIFICATIONS_ROUTE = 'notifications';
2-
31
export const DebounceDelayMs = 1000;
2+
export const DefaultPollingIntervalMs = 5 * 1000;

plugins/notifications/src/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ export {
1212
export { type Notification } from './openapi';
1313

1414
// selected constants for export
15-
export { NOTIFICATIONS_ROUTE } from './constants';
15+
export { notificationsRootRouteRef } from './routes';
1616

1717
// selected components for export
18-
export { NotificationsSidebarItem } from './components/NotificationsSidebarItem';
1918
export { usePollingEffect } from './components/usePollingEffect';
20-
21-
export { default as NotificationsIcon } from '@material-ui/icons/Notifications';
19+
export { NotificationsActiveIcon } from './components/NotificationsActiveIcon';

plugins/notifications/src/plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import {
77
} from '@backstage/core-plugin-api';
88

99
import { NotificationsApiImpl, notificationsApiRef } from './api';
10-
import { rootRouteRef } from './routes';
10+
import { notificationsRootRouteRef } from './routes';
1111

1212
export const notificationsPlugin = createPlugin({
1313
id: 'notifications',
1414
routes: {
15-
root: rootRouteRef,
15+
root: notificationsRootRouteRef,
1616
},
1717
apis: [
1818
createApiFactory({
@@ -33,6 +33,6 @@ export const NotificationsPage = notificationsPlugin.provide(
3333
name: 'NotificationsPage',
3434
component: () =>
3535
import('./components/NotificationsPage').then(m => m.NotificationsPage),
36-
mountPoint: rootRouteRef,
36+
mountPoint: notificationsRootRouteRef,
3737
}),
3838
);

plugins/notifications/src/routes.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { createRouteRef } from '@backstage/core-plugin-api';
22

3-
import { NOTIFICATIONS_ROUTE } from './constants';
4-
5-
export const rootRouteRef = createRouteRef({
6-
id: NOTIFICATIONS_ROUTE,
3+
export const notificationsRootRouteRef = createRouteRef({
4+
id: 'notifications',
75
});

0 commit comments

Comments
 (0)