diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md index 4efe8767c1..643f5a2f23 100644 --- a/plugins/notifications/README.md +++ b/plugins/notifications/README.md @@ -39,7 +39,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( ... {/* New code: */} - + {/* Existing code for reference: */} @@ -55,13 +55,13 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( In the `packages/app/src/App.tsx`: ``` -import { NOTIFICATIONS_ROUTE, NotificationsPage } from '@janus-idp/plugin-notifications'; +import { NotificationsPage } from '@janus-idp/plugin-notifications'; ... export const AppBase = () => { ... {/* New code: */} - } /> + } /> ``` ## How to use the NotificationApi diff --git a/plugins/notifications/app-config.janus-idp.yaml b/plugins/notifications/app-config.janus-idp.yaml index f6ccab6a93..1f3d92f96a 100644 --- a/plugins/notifications/app-config.janus-idp.yaml +++ b/plugins/notifications/app-config.janus-idp.yaml @@ -4,7 +4,7 @@ dynamicPlugins: appIcons: - name: notificationsIcon module: NotificationsPlugin - importName: NotificationsIcon + importName: NotificationsActiveIcon dynamicRoutes: - path: /notifications importName: NotificationsPage diff --git a/plugins/notifications/src/components/NotificationsActiveIcon/NotificationsActiveIcon.tsx b/plugins/notifications/src/components/NotificationsActiveIcon/NotificationsActiveIcon.tsx new file mode 100644 index 0000000000..fc2e03d23a --- /dev/null +++ b/plugins/notifications/src/components/NotificationsActiveIcon/NotificationsActiveIcon.tsx @@ -0,0 +1,99 @@ +import React from 'react'; + +import { configApiRef, useApi } from '@backstage/core-plugin-api'; + +import { Badge, Tooltip } from '@material-ui/core'; +import NotificationsIcon from '@material-ui/icons/Notifications'; +import NotificationsOffIcon from '@material-ui/icons/NotificationsOff'; + +import { notificationsApiRef } from '../../api'; +import { DefaultPollingIntervalMs } from '../../constants'; +import { Notification } from '../../openapi'; +import { usePollingEffect } from '../usePollingEffect'; +import { SystemNotificationAlert } from './SystemNotificationAlert'; + +const NotificationsErrorIcon = () => ( + + + +); + +/** + * Dynamic plugins recently do not support passing configuration + * to icons or making the left-side menu item texts active (so far strings only). + * + * This Icon component tries to workaround these limitations but will be subject of + * change as the extension points by dynamic plugins will evolve. + */ +export const NotificationsActiveIcon = () => { + const notificationsApi = useApi(notificationsApiRef); + const configApi = useApi(configApiRef); + + let pollingInterval = configApi.getOptionalNumber( + 'notifications.pollingIntervalMs', + ); + if (pollingInterval === undefined) { + pollingInterval = DefaultPollingIntervalMs; + } + + const [error, setError] = React.useState(undefined); + const [unreadCount, setUnreadCount] = React.useState(0); + const [pageLoadingTime] = React.useState(new Date(Date.now())); + const [lastSystemWideNotification, setLastSystemWideNotification] = + React.useState(); + const [closedNotificationId, setClosedNotificationId] = + React.useState(); + + const pollCallback = React.useCallback(async () => { + try { + setUnreadCount( + await notificationsApi.getNotificationsCount({ + read: false, + messageScope: 'user', + }), + ); + + const data = await notificationsApi.getNotifications({ + pageSize: 1, + pageNumber: 1, + createdAfter: pageLoadingTime, + orderBy: 'created', + orderByDirec: 'desc', + messageScope: 'system', + }); + + setLastSystemWideNotification(data?.[0]); + } catch (e: unknown) { + setError(e as Error); + } + }, [notificationsApi, pageLoadingTime]); + + usePollingEffect(pollCallback, [], pollingInterval); + + if (!!error) { + return ; + } + + if (unreadCount) { + return ( + <> + + + + + {lastSystemWideNotification && + !lastSystemWideNotification.readByUser && + closedNotificationId !== lastSystemWideNotification.id && ( + + setClosedNotificationId(lastSystemWideNotification.id) + } + /> + )} + + ); + } + + return ; +}; diff --git a/plugins/notifications/src/components/NotificationsActiveIcon/SystemNotificationAlert.tsx b/plugins/notifications/src/components/NotificationsActiveIcon/SystemNotificationAlert.tsx new file mode 100644 index 0000000000..de40cd8149 --- /dev/null +++ b/plugins/notifications/src/components/NotificationsActiveIcon/SystemNotificationAlert.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import { useRouteRef } from '@backstage/core-plugin-api'; + +import { IconButton, Link, makeStyles, Snackbar } from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; + +import { notificationsRootRouteRef } from '../../routes'; + +const useStyles = makeStyles(_theme => ({ + systemAlertAction: { + marginRight: '1rem', + }, +})); + +export type SystemNotificationAlertProps = { + message: string; + onCloseNotification: () => void; +}; + +export const SystemNotificationAlert = ({ + message, + onCloseNotification, +}: SystemNotificationAlertProps) => { + const styles = useStyles(); + const notificationsRoute = useRouteRef(notificationsRootRouteRef); + + return ( + + + Show + + + + + + } + /> + ); +}; diff --git a/plugins/notifications/src/components/NotificationsActiveIcon/index.ts b/plugins/notifications/src/components/NotificationsActiveIcon/index.ts new file mode 100644 index 0000000000..0b758d0073 --- /dev/null +++ b/plugins/notifications/src/components/NotificationsActiveIcon/index.ts @@ -0,0 +1 @@ +export * from './NotificationsActiveIcon'; diff --git a/plugins/notifications/src/components/NotificationsSidebarItem.tsx b/plugins/notifications/src/components/NotificationsSidebarItem.tsx deleted file mode 100644 index d777255ced..0000000000 --- a/plugins/notifications/src/components/NotificationsSidebarItem.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; - -import { SidebarItem } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; - -import { - IconButton, - Link, - makeStyles, - Snackbar, - Tooltip, -} from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; -import NotificationsIcon from '@material-ui/icons/Notifications'; -import NotificationsOffIcon from '@material-ui/icons/NotificationsOff'; - -import { notificationsApiRef } from '../api'; -import { NOTIFICATIONS_ROUTE } from '../constants'; -import { Notification } from '../openapi'; -import { usePollingEffect } from './usePollingEffect'; - -const NotificationsErrorIcon = () => ( - - - -); - -export type NotificationsSidebarItemProps = { - /** - * Number of milliseconds between polling the notifications backend. - * If negative or zero, the poling is not started. - * Example: 5000 - */ - pollingInterval?: number; - className?: string; -}; - -const useStyles = makeStyles(_theme => ({ - systemAlertAction: { - marginRight: '1rem', - }, -})); - -export const NotificationsSidebarItem = ({ - pollingInterval, - className, -}: NotificationsSidebarItemProps) => { - const styles = useStyles(); - const notificationsApi = useApi(notificationsApiRef); - - const [error, setError] = React.useState(undefined); - const [unreadCount, setUnreadCount] = React.useState(0); - const [pageLoadingTime] = React.useState(new Date(Date.now())); - const [lastSystemWideNotification, setLastSystemWideNotification] = - React.useState(); - const [closedNotificationId, setClosedNotificationId] = - React.useState(); - - const pollCallback = React.useCallback(async () => { - try { - setUnreadCount( - await notificationsApi.getNotificationsCount({ - read: false, - messageScope: 'user', - }), - ); - - const data = await notificationsApi.getNotifications({ - pageSize: 1, - pageNumber: 1, - createdAfter: pageLoadingTime, - orderBy: 'created', - orderByDirec: 'desc', - messageScope: 'system', - }); - - setLastSystemWideNotification(data?.[0]); - } catch (e: unknown) { - setError(e as Error); - } - }, [notificationsApi, pageLoadingTime]); - - usePollingEffect(pollCallback, [], pollingInterval); - - let icon = NotificationsIcon; - if (!!error) { - icon = NotificationsErrorIcon; - } - - return ( - <> - - {lastSystemWideNotification && !lastSystemWideNotification.readByUser && ( - - - Show - - - setClosedNotificationId(lastSystemWideNotification.id) - } - > - - - - } - /> - )} - - ); -}; diff --git a/plugins/notifications/src/constants.ts b/plugins/notifications/src/constants.ts index fe0c79a340..53a0804ed7 100644 --- a/plugins/notifications/src/constants.ts +++ b/plugins/notifications/src/constants.ts @@ -1,3 +1,2 @@ -export const NOTIFICATIONS_ROUTE = 'notifications'; - export const DebounceDelayMs = 1000; +export const DefaultPollingIntervalMs = 5 * 1000; diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index 58b3ceeb4c..9579592aa5 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -12,10 +12,8 @@ export { export { type Notification } from './openapi'; // selected constants for export -export { NOTIFICATIONS_ROUTE } from './constants'; +export { notificationsRootRouteRef } from './routes'; // selected components for export -export { NotificationsSidebarItem } from './components/NotificationsSidebarItem'; export { usePollingEffect } from './components/usePollingEffect'; - -export { default as NotificationsIcon } from '@material-ui/icons/Notifications'; +export { NotificationsActiveIcon } from './components/NotificationsActiveIcon'; diff --git a/plugins/notifications/src/plugin.ts b/plugins/notifications/src/plugin.ts index 9a9560bbaf..df8a3e0778 100644 --- a/plugins/notifications/src/plugin.ts +++ b/plugins/notifications/src/plugin.ts @@ -7,12 +7,12 @@ import { } from '@backstage/core-plugin-api'; import { NotificationsApiImpl, notificationsApiRef } from './api'; -import { rootRouteRef } from './routes'; +import { notificationsRootRouteRef } from './routes'; export const notificationsPlugin = createPlugin({ id: 'notifications', routes: { - root: rootRouteRef, + root: notificationsRootRouteRef, }, apis: [ createApiFactory({ @@ -33,6 +33,6 @@ export const NotificationsPage = notificationsPlugin.provide( name: 'NotificationsPage', component: () => import('./components/NotificationsPage').then(m => m.NotificationsPage), - mountPoint: rootRouteRef, + mountPoint: notificationsRootRouteRef, }), ); diff --git a/plugins/notifications/src/routes.ts b/plugins/notifications/src/routes.ts index d07f175254..fbe0487184 100644 --- a/plugins/notifications/src/routes.ts +++ b/plugins/notifications/src/routes.ts @@ -1,7 +1,5 @@ import { createRouteRef } from '@backstage/core-plugin-api'; -import { NOTIFICATIONS_ROUTE } from './constants'; - -export const rootRouteRef = createRouteRef({ - id: NOTIFICATIONS_ROUTE, +export const notificationsRootRouteRef = createRouteRef({ + id: 'notifications', });