feat: integrate notifications with signals

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2024-01-25 09:07:16 +02:00
parent 1a38100a8e
commit 6a79f9f1ee
17 changed files with 98 additions and 36 deletions
+1
View File
@@ -109,6 +109,7 @@ function makeCreateEnv(config: Config) {
logger: root.child({ type: 'plugin' }),
discovery,
tokenManager,
signalService,
});
root.info(`Created UrlReader ${reader}`);
@@ -36,5 +36,6 @@ export default async function createPlugin(
tokenManager: env.tokenManager,
database: env.database,
discovery: env.discovery,
signalService: env.signalService,
});
}
+3 -1
View File
@@ -4,7 +4,8 @@ Welcome to the notifications backend plugin!
## Getting started
First you have to install the `@backstage/plugin-notifications-node` package.
First you have to install `@backstage/plugin-notifications-node` and `@backstage/plugin-signals-node`
packages.
Then create a new file to `packages/backend/src/plugins/notifications.ts`:
@@ -22,6 +23,7 @@ export default async function createPlugin(
tokenManager: env.tokenManager,
database: env.database,
discovery: env.discovery,
signalService: env.signalService
});
}
```
+5 -2
View File
@@ -5,12 +5,13 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CatalogApi } from '@backstage/catalog-client';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { DiscoveryService } from '@backstage/backend-plugin-api';
import express from 'express';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { SignalService } from '@backstage/plugin-signals-node';
import { TokenManager } from '@backstage/backend-common';
// @public (undocumented)
@@ -32,7 +33,7 @@ export interface RouterOptions {
// (undocumented)
database: PluginDatabaseManager;
// (undocumented)
discovery: DiscoveryApi;
discovery: DiscoveryService;
// (undocumented)
identity: IdentityApi;
// (undocumented)
@@ -40,6 +41,8 @@ export interface RouterOptions {
// (undocumented)
processors?: NotificationProcessor[];
// (undocumented)
signalService?: SignalService;
// (undocumented)
tokenManager: TokenManager;
}
+2 -1
View File
@@ -27,11 +27,12 @@
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"@backstage/plugin-notifications-node": "workspace:^",
"@backstage/plugin-signals-node": "workspace:^",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
@@ -18,6 +18,7 @@ import {
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './service/router';
import { signalService } from '@backstage/plugin-signals-node';
/**
* Notifications backend plugin
@@ -35,6 +36,7 @@ export const notificationsPlugin = createBackendPlugin({
database: coreServices.database,
tokenManager: coreServices.tokenManager,
discovery: coreServices.discovery,
signals: signalService,
},
async init({
httpRouter,
@@ -43,6 +45,7 @@ export const notificationsPlugin = createBackendPlugin({
database,
tokenManager,
discovery,
signals,
}) {
httpRouter.use(
await createRouter({
@@ -51,6 +54,7 @@ export const notificationsPlugin = createBackendPlugin({
database,
tokenManager,
discovery,
signalService: signals,
}),
);
},
@@ -26,6 +26,7 @@ import request from 'supertest';
import { createRouter } from './router';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { ConfigReader } from '@backstage/config';
import { SignalService } from '@backstage/plugin-signals-node';
function createDatabase(): PluginDatabaseManager {
return DatabaseManager.fromConfig(
@@ -65,6 +66,10 @@ describe('createRouter', () => {
getExternalBaseUrl: jest.fn(),
};
const signalService: jest.Mocked<SignalService> = {
publish: jest.fn(),
};
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
@@ -72,6 +77,7 @@ describe('createRouter', () => {
database: createDatabase(),
tokenManager: mockedTokenManager,
discovery,
signalService,
});
app = express().use(router);
});
@@ -39,8 +39,8 @@ import {
} from '@backstage/catalog-model';
import { NotificationProcessor } from '../types';
import { AuthenticationError } from '@backstage/errors';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
import { SignalService } from '@backstage/plugin-signals-node';
/** @public */
export interface RouterOptions {
@@ -48,7 +48,8 @@ export interface RouterOptions {
identity: IdentityApi;
database: PluginDatabaseManager;
tokenManager: TokenManager;
discovery: DiscoveryApi;
discovery: DiscoveryService;
signalService?: SignalService;
catalog?: CatalogApi;
processors?: NotificationProcessor[];
}
@@ -65,6 +66,7 @@ export async function createRouter(
catalog,
tokenManager,
processors,
signalService,
} = options;
const catalogClient =
@@ -248,7 +250,14 @@ export async function createRouter(
}
}
notifications.push(notification);
// TODO: Signal service
}
if (signalService) {
await signalService.publish({
recipients: users,
message: { action: 'refresh' },
channel: 'notifications',
});
}
res.send(notifications);
@@ -31,6 +31,12 @@ import {
CatalogRequestOptions,
GetEntitiesByRefsRequest,
} from '@backstage/catalog-client';
import { DefaultSignalService } from '@backstage/plugin-signals-node';
import {
EventBroker,
EventParams,
EventSubscriber,
} from '@backstage/plugin-events-node';
export interface ServerOptions {
port: number;
@@ -90,6 +96,20 @@ export async function startStandaloneServer(
},
};
const mockSubscribers: EventSubscriber[] = [];
const eventBroker: EventBroker = {
async publish(params: EventParams): Promise<void> {
mockSubscribers.forEach(sub => sub.onEvent(params));
},
subscribe(...subscribers: EventSubscriber[]) {
subscribers.flat().forEach(subscriber => {
mockSubscribers.push(subscriber);
});
},
};
const signalService = DefaultSignalService.create({ eventBroker });
const router = await createRouter({
logger,
identity: identityMock,
@@ -97,6 +117,7 @@ export async function startStandaloneServer(
catalog: catalogApi,
discovery,
tokenManager,
signalService,
});
let service = createServiceBuilder(module)
+4 -2
View File
@@ -3,10 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { DiscoveryService } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { SignalService } from '@backstage/plugin-signals-node';
import { TokenManager } from '@backstage/backend-common';
// @public (undocumented)
@@ -40,7 +41,8 @@ export const notificationService: ServiceRef<NotificationService, 'plugin'>;
// @public (undocumented)
export type NotificationServiceOptions = {
logger: LoggerService;
discovery: PluginEndpointDiscovery;
discovery: DiscoveryService;
tokenManager: TokenManager;
signalService: SignalService;
};
```
+1
View File
@@ -33,6 +33,7 @@
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"@backstage/plugin-signals-node": "workspace:^",
"knex": "^3.0.0",
"uuid": "^8.0.0"
}
+4 -2
View File
@@ -20,6 +20,7 @@ import {
} from '@backstage/backend-plugin-api';
import { DefaultNotificationService } from './service';
import { NotificationService } from './service/NotificationService';
import { signalService } from '@backstage/plugin-signals-node';
/** @public */
export const notificationService = createServiceRef<NotificationService>({
@@ -32,13 +33,14 @@ export const notificationService = createServiceRef<NotificationService>({
logger: coreServices.logger,
discovery: coreServices.discovery,
tokenManager: coreServices.tokenManager,
// TODO: Signals
signals: signalService,
},
factory({ logger, discovery, tokenManager }) {
factory({ logger, discovery, tokenManager, signals }) {
return DefaultNotificationService.create({
logger,
discovery,
tokenManager,
signalService: signals,
});
},
}),
@@ -14,18 +14,17 @@
* limitations under the License.
*/
import { Notification } from '@backstage/plugin-notifications-common';
import {
PluginEndpointDiscovery,
TokenManager,
} from '@backstage/backend-common';
import { TokenManager } from '@backstage/backend-common';
import { NotificationService } from './NotificationService';
import { LoggerService } from '@backstage/backend-plugin-api';
import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api';
import { SignalService } from '@backstage/plugin-signals-node';
/** @public */
export type NotificationServiceOptions = {
logger: LoggerService;
discovery: PluginEndpointDiscovery;
discovery: DiscoveryService;
tokenManager: TokenManager;
signalService: SignalService;
};
/** @public */
@@ -40,7 +39,7 @@ export type NotificationSendOptions = {
export class DefaultNotificationService implements NotificationService {
private constructor(
private readonly logger: LoggerService,
private readonly discovery: PluginEndpointDiscovery,
private readonly discovery: DiscoveryService,
private readonly tokenManager: TokenManager,
) {}
+1
View File
@@ -27,6 +27,7 @@
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"@backstage/plugin-signals-react": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Content,
ErrorPanel,
@@ -27,6 +27,7 @@ import Bookmark from '@material-ui/icons/Bookmark';
import Check from '@material-ui/icons/Check';
import Inbox from '@material-ui/icons/Inbox';
import { NotificationType } from '@backstage/plugin-notifications-common';
import { useSignal } from '@backstage/plugin-signals-react';
const useStyles = makeStyles(_theme => ({
filterButton: {
@@ -43,6 +44,13 @@ export const NotificationsPage = () => {
[type],
);
const { lastSignal } = useSignal('notifications');
useEffect(() => {
if (lastSignal && lastSignal.action === 'refresh') {
retry();
}
}, [lastSignal, retry]);
const onUpdate = () => {
retry();
};
@@ -19,25 +19,30 @@ import { SidebarItem } from '@backstage/core-components';
import NotificationsIcon from '@material-ui/icons/Notifications';
import { useRouteRef } from '@backstage/core-plugin-api';
import { rootRouteRef } from '../../routes';
import { useSignal } from '@backstage/plugin-signals-react';
/** @public */
export const NotificationsSidebarItem = () => {
const {
loading,
error,
value,
retry: _retry,
} = useNotificationsApi(api => api.getStatus());
const { loading, error, value, retry } = useNotificationsApi(api =>
api.getStatus(),
);
const [unreadCount, setUnreadCount] = React.useState(0);
const notificationsRoute = useRouteRef(rootRouteRef);
const { lastSignal } = useSignal('notifications');
useEffect(() => {
if (lastSignal && lastSignal.action === 'refresh') {
retry();
}
}, [lastSignal, retry]);
useEffect(() => {
if (!loading && !error && value) {
setUnreadCount(value.unread);
}
}, [loading, error, value]);
// TODO: Figure out if there count can be added to hasNotifications
// TODO: Figure out if the count can be added to hasNotifications
return (
<SidebarItem
icon={NotificationsIcon}
+5 -9
View File
@@ -7789,11 +7789,12 @@ __metadata:
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
"@backstage/plugin-notifications-node": "workspace:^"
"@backstage/plugin-signals-node": "workspace:^"
"@types/express": "*"
"@types/supertest": ^2.0.8
express: ^4.17.1
@@ -7827,6 +7828,7 @@ __metadata:
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
"@backstage/plugin-signals-node": "workspace:^"
knex: ^3.0.0
uuid: ^8.0.0
languageName: unknown
@@ -7843,6 +7845,7 @@ __metadata:
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
"@backstage/plugin-signals-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.9.13
@@ -25320,20 +25323,13 @@ __metadata:
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.5.6":
"dom-accessibility-api@npm:^0.5.6, dom-accessibility-api@npm:^0.5.9":
version: 0.5.16
resolution: "dom-accessibility-api@npm:0.5.16"
checksum: 005eb283caef57fc1adec4d5df4dd49189b628f2f575af45decb210e04d634459e3f1ee64f18b41e2dcf200c844bc1d9279d80807e686a30d69a4756151ad248
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.5.9":
version: 0.5.13
resolution: "dom-accessibility-api@npm:0.5.13"
checksum: a5a5f14c01e466d424750aaac9225f1dc43cf16d101a1c40e01a554abce63c48084707002c39b805f2ce212273c179dd6d2258175997cd06d5f79851bf52dd40
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.6.3":
version: 0.6.3
resolution: "dom-accessibility-api@npm:0.6.3"