feat: initial notifications support

Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
Heikki Hellgren
2023-12-15 14:05:35 +02:00
parent 778ea9e152
commit f24a0c1f6a
59 changed files with 1964 additions and 7 deletions
+1
View File
@@ -59,6 +59,7 @@
"@backstage/plugin-newrelic": "workspace:^",
"@backstage/plugin-newrelic-dashboard": "workspace:^",
"@backstage/plugin-nomad": "workspace:^",
"@backstage/plugin-notifications": "^0.0.0",
"@backstage/plugin-octopus-deploy": "workspace:^",
"@backstage/plugin-org": "workspace:^",
"@backstage/plugin-pagerduty": "workspace:^",
+4 -2
View File
@@ -67,15 +67,15 @@ import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import {
TechDocsIndexPage,
TechDocsReaderPage,
techdocsPlugin,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import {
ExpandableNavigation,
LightBox,
ReportIssue,
TextSize,
LightBox,
} from '@backstage/plugin-techdocs-module-addons-contrib';
import {
SettingsLayout,
@@ -107,6 +107,7 @@ import { PuppetDbPage } from '@backstage/plugin-puppetdb';
import { DevToolsPage } from '@backstage/plugin-devtools';
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities';
import { NotificationsPage } from '@backstage/plugin-notifications';
const app = createApp({
apis,
@@ -272,6 +273,7 @@ const routes = (
<Route path="/devtools" element={<DevToolsPage />}>
{customDevToolsPage}
</Route>
<Route path="/notifications" element={<NotificationsPage />} />
</FlatRoutes>
);
+5 -2
View File
@@ -34,6 +34,7 @@ import {
import { SidebarSearchModal } from '@backstage/plugin-search';
import { Shortcuts } from '@backstage/plugin-shortcuts';
import {
Link,
Sidebar,
sidebarConfig,
SidebarDivider,
@@ -42,16 +43,16 @@ import {
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
Link,
useSidebarOpenState,
SidebarSubmenu,
SidebarSubmenuItem,
useSidebarOpenState,
} from '@backstage/core-components';
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import { SearchModal } from '../search/SearchModal';
import Score from '@material-ui/icons/Score';
import { useApp } from '@backstage/core-plugin-api';
import BuildIcon from '@material-ui/icons/Build';
import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -166,6 +167,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
</SidebarScrollWrapper>
<SidebarDivider />
<Shortcuts allowExternalLinks />
<SidebarDivider />
<NotificationsSidebarItem />
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
+2
View File
@@ -55,6 +55,8 @@
"@backstage/plugin-lighthouse-backend": "workspace:^",
"@backstage/plugin-linguist-backend": "workspace:^",
"@backstage/plugin-nomad-backend": "workspace:^",
"@backstage/plugin-notifications-backend": "^0.0.0",
"@backstage/plugin-notifications-node": "workspace:^",
"@backstage/plugin-permission-backend": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
+11
View File
@@ -66,6 +66,7 @@ import linguist from './plugins/linguist';
import devTools from './plugins/devtools';
import nomad from './plugins/nomad';
import signals from './plugins/signals';
import notifications from './plugins/notifications';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
@@ -74,6 +75,7 @@ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
import { MeterProvider } from '@opentelemetry/sdk-metrics';
import { metrics } from '@opentelemetry/api';
import { DefaultSignalService } from '@backstage/plugin-signals-node';
import { NotificationService } from '@backstage/plugin-notifications-node';
// Expose opentelemetry metrics using a Prometheus exporter on
// http://localhost:9464/metrics . See prometheus.yml in packages/backend for
@@ -103,6 +105,10 @@ function makeCreateEnv(config: Config) {
const signalService = DefaultSignalService.create({
eventBroker,
});
const notificationService = NotificationService.create({
database: databaseManager.forPlugin('notifications'),
discovery,
});
root.info(`Created UrlReader ${reader}`);
@@ -125,6 +131,7 @@ function makeCreateEnv(config: Config) {
scheduler,
identity,
signalService,
notificationService,
};
};
}
@@ -179,6 +186,9 @@ async function main() {
const devToolsEnv = useHotMemoize(module, () => createEnv('devtools'));
const nomadEnv = useHotMemoize(module, () => createEnv('nomad'));
const signalsEnv = useHotMemoize(module, () => createEnv('signals'));
const notificationsEnv = useHotMemoize(module, () =>
createEnv('notifications'),
);
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -206,6 +216,7 @@ async function main() {
apiRouter.use('/devtools', await devTools(devToolsEnv));
apiRouter.use('/nomad', await nomad(nomadEnv));
apiRouter.use('/signals', await signals(signalsEnv));
apiRouter.use('/notifications', await notifications(notificationsEnv));
apiRouter.use(notFoundHandler());
await lighthouse(lighthouseEnv);
@@ -0,0 +1,39 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouter } from '@backstage/plugin-notifications-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
// TODO: Remove this test code
// setInterval(() => {
// env.notificationService.send(
// 'user:default/guest',
// 'Test',
// 'This is test notification',
// '/catalog',
// );
// }, 60000);
return await createRouter({
logger: env.logger,
identity: env.identity,
notificationService: env.notificationService,
});
}
+4 -2
View File
@@ -27,7 +27,8 @@ import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { EventBroker } from '@backstage/plugin-events-node';
import { DefaultSignalService } from '@backstage/plugin-signals-node';
import { SignalService } from '@backstage/plugin-signals-node';
import { NotificationService } from '@backstage/plugin-notifications-node';
export type PluginEnvironment = {
logger: Logger;
@@ -41,5 +42,6 @@ export type PluginEnvironment = {
scheduler: PluginTaskScheduler;
identity: IdentityApi;
eventBroker: EventBroker;
signalService: DefaultSignalService;
signalService: SignalService;
notificationService: NotificationService;
};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+14
View File
@@ -0,0 +1,14 @@
# notifications
Welcome to the notifications backend plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn
start` in the root directory, and then navigating to [/notifications](http://localhost:3000/notifications).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory.
@@ -0,0 +1,25 @@
## API Report File for "@backstage/plugin-notifications-backend"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import express from 'express';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { Logger } from 'winston';
import { NotificationService } from '@backstage/plugin-notifications-node';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise<express.Router>;
// @public (undocumented)
export interface RouterOptions {
// (undocumented)
identity: IdentityApi;
// (undocumented)
logger: Logger;
// (undocumented)
notificationService: NotificationService;
}
// (No @packageDocumentation comment for this package)
```
@@ -0,0 +1,46 @@
{
"name": "@backstage/plugin-notifications-backend",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-notifications-node": "workspace:^",
"@types/express": "*",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"knex": "^3.0.0",
"node-fetch": "^2.6.7",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
"msw": "^1.0.0",
"supertest": "^6.2.4"
},
"files": [
"dist"
]
}
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './service/router';
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getRootLogger } from '@backstage/backend-common';
import yn from 'yn';
import { startStandaloneServer } from './service/standaloneServer';
const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
const logger = getRootLogger();
startStandaloneServer({ port, enableCors, logger }).catch(err => {
logger.error(err);
process.exit(1);
});
process.on('SIGINT', () => {
logger.info('CTRL+C pressed; exiting.');
process.exit(0);
});
@@ -0,0 +1,66 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import express from 'express';
import request from 'supertest';
import { createRouter } from './router';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { NotificationService } from '@backstage/plugin-notifications-node';
describe('createRouter', () => {
let app: express.Express;
const identityMock: IdentityApi = {
async getIdentity() {
return {
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'user:default/guest',
},
token: 'no-token',
};
},
};
const notificationServiceMock: jest.Mocked<Partial<NotificationService>> = {
getStore: jest.fn(),
send: jest.fn(),
};
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
identity: identityMock,
notificationService: notificationServiceMock as NotificationService,
});
app = express().use(router);
});
beforeEach(() => {
jest.resetAllMocks();
});
describe('GET /health', () => {
it('returns ok', async () => {
const response = await request(app).get('/health');
expect(response.status).toEqual(200);
expect(response.body).toEqual({ status: 'ok' });
});
});
});
@@ -0,0 +1,67 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { errorHandler } from '@backstage/backend-common';
import express, { Request } from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { NotificationService } from '@backstage/plugin-notifications-node';
import { IdentityApi } from '@backstage/plugin-auth-node';
/** @public */
export interface RouterOptions {
logger: Logger;
identity: IdentityApi;
notificationService: NotificationService;
}
/** @public */
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const { logger, notificationService, identity } = options;
const store = await notificationService.getStore();
const getUser = async (req: Request<unknown>) => {
const user = await identity.getIdentity({ request: req });
return user ? user.identity.userEntityRef : 'user:default/guest';
};
const router = Router();
router.use(express.json());
router.get('/health', (_, response) => {
logger.info('PONG!');
response.json({ status: 'ok' });
});
router.get('/notifications', async (req, res) => {
const user = await getUser(req);
const notifications = await store.getNotifications({ user_ref: user });
res.send(notifications);
});
router.get('/status', async (req, res) => {
const user = await getUser(req);
const status = await store.getStatus({ user_ref: user });
res.send(status);
});
// TODO: Add endpoint to set read/unread by notification id(s)
router.use(errorHandler());
return router;
}
@@ -0,0 +1,99 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createServiceBuilder,
loadBackendConfig,
PluginDatabaseManager,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createRouter } from './router';
import Knex from 'knex';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { Request } from 'express';
import { NotificationService } from '@backstage/plugin-notifications-node';
export interface ServerOptions {
port: number;
enableCors: boolean;
logger: Logger;
}
export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'notifications-backend' });
logger.debug('Starting application server...');
const config = await loadBackendConfig({ logger, argv: process.argv });
const db = Knex(config.get('backend.database'));
const dbMock: PluginDatabaseManager = {
async getClient() {
return db;
},
};
const discoveryMock: PluginEndpointDiscovery = {
async getBaseUrl(pluginId: string) {
return `http://localhost:7007/api/${pluginId}`;
},
async getExternalBaseUrl(pluginId: string) {
return `http://localhost:7007/api/${pluginId}`;
},
};
const notificationService = NotificationService.create({
database: dbMock,
discovery: discoveryMock,
});
const identityMock: IdentityApi = {
async getIdentity({ request }: { request: Request<unknown> }) {
const token = request.headers.authorization?.split(' ')[1];
return {
identity: {
type: 'user',
ownershipEntityRefs: [],
userEntityRef: 'user:default/guest',
},
token: token || 'no-token',
};
},
};
const router = await createRouter({
logger,
identity: identityMock,
notificationService,
});
let service = createServiceBuilder(module)
.setPort(options.port)
.addRouter('/notifications', router);
if (options.enableCors) {
service = service.enableCors({ origin: 'http://localhost:3000' });
}
return await service.start().catch(err => {
logger.error(err);
process.exit(1);
});
}
module.hot?.accept();
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+5
View File
@@ -0,0 +1,5 @@
# @backstage/plugin-notifications-common
Welcome to the common package for the notifications plugin!
_This plugin was created through the Backstage CLI_
@@ -0,0 +1,24 @@
## API Report File for "@backstage/plugin-notifications-common"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
// @public (undocumented)
type Notification_2 = {
id: string;
userRef: string;
title: string;
description: string;
link: string;
icon?: string;
created: Date;
read?: Date;
};
export { Notification_2 as Notification };
// @public (undocumented)
export type NotificationStatus = {
unread: number;
read: number;
};
```
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@backstage/plugin-notifications-common",
"description": "Common functionalities for the notifications plugin",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "common-library"
},
"sideEffects": false,
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
]
}
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Common functionalities for the notifications plugin.
*
* @packageDocumentation
*/
export * from './types';
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** @public */
export type Notification = {
id: string;
userRef: string;
title: string;
description: string;
link: string;
// TODO: Icon should be typed so that we know what to render
icon?: string;
created: Date;
read?: Date;
};
/** @public */
export type NotificationStatus = {
unread: number;
read: number;
};
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+5
View File
@@ -0,0 +1,5 @@
# @backstage/plugin-notifications-node
Welcome to the Node.js library package for the notifications plugin!
_This plugin was created through the Backstage CLI_
+70
View File
@@ -0,0 +1,70 @@
## API Report File for "@backstage/plugin-notifications-node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { NotificationStatus } from '@backstage/plugin-notifications-common';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
// @public (undocumented)
export class DatabaseNotificationsStore implements NotificationsStore {
// (undocumented)
static create({
database,
skipMigrations,
}: {
database: PluginDatabaseManager;
skipMigrations?: boolean;
}): Promise<DatabaseNotificationsStore>;
// (undocumented)
getNotifications(options: NotificationGetOptions): Promise<any[]>;
// (undocumented)
getStatus(options: NotificationGetOptions): Promise<{
unread: number;
read: number;
}>;
// (undocumented)
saveNotification(notification: Notification_2): Promise<void>;
}
// @public (undocumented)
export type NotificationGetOptions = {
user_ref: string;
};
// @public (undocumented)
export class NotificationService {
// (undocumented)
static create({
database,
discovery,
}: NotificationServiceOptions): NotificationService;
// (undocumented)
getStore(): Promise<NotificationsStore>;
// (undocumented)
send(
entityRef: string | string[],
title: string,
description: string,
link: string,
): Promise<Notification_2[]>;
}
// @public (undocumented)
export type NotificationServiceOptions = {
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
};
// @public (undocumented)
export interface NotificationsStore {
// (undocumented)
getNotifications(options: NotificationGetOptions): Promise<Notification_2[]>;
// (undocumented)
getStatus(options: NotificationGetOptions): Promise<NotificationStatus>;
// (undocumented)
saveNotification(notification: Notification_2): Promise<void>;
}
```
@@ -0,0 +1,34 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.up = async function up(knex) {
await knex.schema.createTable('notifications', table => {
table.uuid('id').primary();
table.string('userRef').notNullable();
table.string('title').notNullable();
table.text('description').notNullable();
table.text('link').notNullable();
table.datetime('created').notNullable();
table.datetime('read').nullable();
});
};
/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.dropTable('notifications');
};
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@backstage/plugin-notifications-node",
"description": "Node.js library for the notifications plugin",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"devDependencies": {
"@backstage/cli": "workspace:^"
},
"files": [
"dist"
],
"dependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/catalog-client": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"knex": "^3.0.0",
"uuid": "^8.0.0"
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
PluginDatabaseManager,
resolvePackagePath,
} from '@backstage/backend-common';
import {
NotificationGetOptions,
NotificationsStore,
} from './NotificationsStore';
import { Notification } from '@backstage/plugin-notifications-common';
import { Knex } from 'knex';
const migrationsDir = resolvePackagePath(
'@backstage/plugin-notifications-node',
'migrations',
);
/** @public */
export class DatabaseNotificationsStore implements NotificationsStore {
private constructor(private readonly db: Knex) {}
static async create({
database,
skipMigrations,
}: {
database: PluginDatabaseManager;
skipMigrations?: boolean;
}): Promise<DatabaseNotificationsStore> {
const client = await database.getClient();
if (!database.migrations?.skip && !skipMigrations) {
await client.migrate.latest({
directory: migrationsDir,
});
}
return new DatabaseNotificationsStore(client);
}
private mapToInteger = (val: string | number | undefined): number => {
return typeof val === 'string' ? Number.parseInt(val, 10) : val ?? 0;
};
async getNotifications(options: NotificationGetOptions) {
const { user_ref } = options;
const notificationQuery = this.db('notifications').where(
'userRef',
user_ref,
);
const notifications = await notificationQuery.select('*');
return notifications;
}
async saveNotification(notification: Notification) {
await this.db.insert(notification).into('notifications');
}
async getStatus(options: NotificationGetOptions) {
const { user_ref } = options;
const notificationQuery = this.db('notifications').where(
'userRef',
user_ref,
);
const unreadQuery = await notificationQuery
.clone()
.whereNull('read')
.count('id as UNREAD')
.first();
const readQuery = await notificationQuery
.clone()
.whereNotNull('read')
.count('id as READ')
.first();
return {
unread: this.mapToInteger((unreadQuery as any)?.UNREAD),
read: this.mapToInteger((readQuery as any)?.READ),
};
}
}
@@ -0,0 +1,36 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Notification,
NotificationStatus,
} from '@backstage/plugin-notifications-common';
/** @public */
export type NotificationGetOptions = {
user_ref: string;
};
/** @public */
export interface NotificationsStore {
getNotifications(options: NotificationGetOptions): Promise<Notification[]>;
saveNotification(notification: Notification): Promise<void>;
getStatus(options: NotificationGetOptions): Promise<NotificationStatus>;
// TODO: Mark as read/unread by notification id(s)
}
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './DatabaseNotificationsStore';
export * from './NotificationsStore';
+24
View File
@@ -0,0 +1,24 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Node.js library for the notifications plugin.
*
* @packageDocumentation
*/
export * from './database';
export * from './service';
@@ -0,0 +1,136 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Notification } from '@backstage/plugin-notifications-common';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { NotificationsStore } from '../database/NotificationsStore';
import { v4 as uuid } from 'uuid';
import {
Entity,
isGroupEntity,
isUserEntity,
RELATION_HAS_MEMBER,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
PluginDatabaseManager,
PluginEndpointDiscovery,
} from '@backstage/backend-common';
import { DatabaseNotificationsStore } from '../database';
/** @public */
export type NotificationServiceOptions = {
database: PluginDatabaseManager;
discovery: PluginEndpointDiscovery;
};
/** @public */
export class NotificationService {
private store: NotificationsStore | null = null;
private constructor(
private readonly database: PluginDatabaseManager,
private readonly catalog: CatalogApi,
) {}
static create({
database,
discovery,
}: NotificationServiceOptions): NotificationService {
const catalogClient = new CatalogClient({
discoveryApi: discovery,
});
return new NotificationService(database, catalogClient);
}
async send(
entityRef: string | string[],
title: string,
description: string,
link: string,
): Promise<Notification[]> {
const users = await this.getUsersForEntityRef(entityRef);
const notifications = [];
const store = await this.getStore();
for (const user of users) {
const notification = {
id: uuid(),
userRef: user,
title,
description,
link,
created: new Date(),
};
await store.saveNotification(notification);
notifications.push(notification);
}
// TODO: Signal service
// TODO: Other senders
return notifications;
}
async getStore(): Promise<NotificationsStore> {
if (!this.store) {
this.store = await DatabaseNotificationsStore.create({
database: this.database,
});
}
return this.store;
}
private async getUsersForEntityRef(
entityRef: string | string[],
): Promise<string[]> {
const refs = Array.isArray(entityRef) ? entityRef : [entityRef];
const entities = await this.catalog.getEntitiesByRefs({ entityRefs: refs });
const mapEntity = async (entity: Entity | undefined): Promise<string[]> => {
if (!entity) {
return [];
}
if (isUserEntity(entity)) {
return [stringifyEntityRef(entity)];
} else if (isGroupEntity(entity) && entity.relations) {
return entity.relations
.filter(
relation =>
relation.type === RELATION_HAS_MEMBER && relation.targetRef,
)
.map(r => r.targetRef);
} else if (!isGroupEntity(entity) && entity.spec?.owner) {
const owner = await this.catalog.getEntityByRef(
entity.spec.owner as string,
);
if (owner) {
return mapEntity(owner);
}
}
return [];
};
const users: string[] = [];
for (const entity of entities.items) {
const u = await mapEntity(entity);
users.push(...u);
}
return users;
}
}
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './NotificationService';
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
+1
View File
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+13
View File
@@ -0,0 +1,13 @@
# notifications
Welcome to the notifications plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/notifications](http://localhost:3000/notifications).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory.
+88
View File
@@ -0,0 +1,88 @@
## API Report File for "@backstage/plugin-notifications"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react';
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
import { NotificationStatus } from '@backstage/plugin-notifications-common';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
// @public (undocumented)
export interface NotificationsApi {
// (undocumented)
getNotifications(): Promise<Notification_2[]>;
// (undocumented)
getStatus(): Promise<NotificationStatus>;
}
// @public (undocumented)
export const notificationsApiRef: ApiRef<NotificationsApi>;
// @public (undocumented)
export class NotificationsClient implements NotificationsApi {
constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi });
// (undocumented)
getNotifications(): Promise<Notification_2[]>;
// (undocumented)
getStatus(): Promise<NotificationStatus>;
}
// @public (undocumented)
export const NotificationsPage: () => JSX_2.Element;
// @public (undocumented)
export const notificationsPlugin: BackstagePlugin<
{
root: RouteRef<undefined>;
},
{}
>;
// @public (undocumented)
export const NotificationsSidebarItem: () => React_2.JSX.Element;
// @public (undocumented)
export const NotificationsTable: (props: {
notifications?: Notification_2[];
}) => React_2.JSX.Element;
// @public (undocumented)
export function useNotificationsApi<T>(
f: (api: NotificationsApi) => Promise<T>,
deps?: any[],
):
| {
retry: () => void;
loading: boolean;
error?: undefined;
value?: undefined;
}
| {
retry: () => void;
loading: false;
error: Error;
value?: undefined;
}
| {
retry: () => void;
loading: true;
error?: Error | undefined;
value?: T | undefined;
}
| {
retry: () => void;
loading: false;
error?: undefined;
value: T;
};
// (No @packageDocumentation comment for this package)
```
+27
View File
@@ -0,0 +1,27 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { notificationsPlugin } from '../src/plugin';
createDevApp()
.registerPlugin(notificationsPlugin)
.addPage({
element: <div />,
title: 'Root Page',
path: '/notifications',
})
.render();
+53
View File
@@ -0,0 +1,53 @@
{
"name": "@backstage/plugin-notifications",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"sideEffects": false,
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-notifications-common": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"msw": "^1.0.0"
},
"files": [
"dist"
]
}
@@ -0,0 +1,34 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '@backstage/core-plugin-api';
import {
Notification,
NotificationStatus,
} from '@backstage/plugin-notifications-common';
/** @public */
export const notificationsApiRef = createApiRef<NotificationsApi>({
id: 'plugin.notifications.service',
});
/** @public */
export interface NotificationsApi {
getNotifications(): Promise<Notification[]>;
getStatus(): Promise<NotificationStatus>;
// TODO: Mark as read/unread by notification id(s)
}
@@ -0,0 +1,57 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NotificationsApi } from './NotificationsApi';
import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
import {
Notification,
NotificationStatus,
} from '@backstage/plugin-notifications-common';
/** @public */
export class NotificationsClient implements NotificationsApi {
private readonly discoveryApi: DiscoveryApi;
private readonly fetchApi: FetchApi;
public constructor(options: {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
}) {
this.discoveryApi = options.discoveryApi;
this.fetchApi = options.fetchApi;
}
async getNotifications(): Promise<Notification[]> {
return await this.get<Notification[]>('notifications');
}
async getStatus(): Promise<NotificationStatus> {
return await this.get<NotificationStatus>('status');
}
private async get<T>(path: string): Promise<T> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('notifications')}/`;
const url = new URL(path, baseUrl);
const response = await this.fetchApi.fetch(url.toString());
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<T>;
}
}
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './NotificationsApi';
export * from './NotificationsClient';
@@ -0,0 +1,82 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
Content,
ErrorPanel,
PageWithHeader,
} from '@backstage/core-components';
import { NotificationsTable } from '../NotificationsTable';
import { useNotificationsApi } from '../../hooks';
import {
Button,
Grid,
makeStyles,
Paper,
TableContainer,
} from '@material-ui/core';
import Bookmark from '@material-ui/icons/Bookmark';
import Check from '@material-ui/icons/Check';
import Inbox from '@material-ui/icons/Inbox';
const useStyles = makeStyles(_theme => ({
filterButton: {
width: '100%',
justifyContent: 'start',
},
}));
export const NotificationsPage = () => {
const {
loading: _loading,
error,
value,
retry: _retry,
} = useNotificationsApi(api => api.getNotifications());
const styles = useStyles();
if (error) {
return <ErrorPanel error={new Error('Failed to load notifications')} />;
}
// TODO: Make the filter buttons work
// TODO: Add signals listener and refresh data on message
return (
<PageWithHeader title="Notifications" themeId="tool">
<Content>
<Grid container>
<Grid item xs={2}>
<Button className={styles.filterButton} startIcon={<Inbox />}>
Inbox
</Button>
<Button className={styles.filterButton} startIcon={<Check />}>
Done
</Button>
<Button className={styles.filterButton} startIcon={<Bookmark />}>
Saved
</Button>
</Grid>
<Grid item xs={10}>
<TableContainer component={Paper}>
<NotificationsTable notifications={value} />
</TableContainer>
</Grid>
</Grid>
</Content>
</PageWithHeader>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './NotificationsPage';
@@ -0,0 +1,49 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 { rootRouteRef } from '../../routes';
/** @public */
export const NotificationsSidebarItem = () => {
const {
loading,
error,
value,
retry: _retry,
} = useNotificationsApi(api => api.getStatus());
const [unreadCount, setUnreadCount] = React.useState(0);
const notificationsRoute = useRouteRef(rootRouteRef);
useEffect(() => {
if (!loading && !error && value) {
setUnreadCount(value.unread);
}
}, [loading, error, value]);
// TODO: Figure out if there count can be added to hasNotifications
return (
<SidebarItem
icon={NotificationsIcon}
to={notificationsRoute()}
text="Notifications"
hasNotifications={!error && !!unreadCount}
/>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './NotificationsSideBarItem';
@@ -0,0 +1,100 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
IconButton,
makeStyles,
Table,
TableCell,
TableHead,
TableRow,
Tooltip,
Typography,
} from '@material-ui/core';
import { Notification } from '@backstage/plugin-notifications-common';
import { useNavigate } from 'react-router-dom';
import NotificationsIcon from '@material-ui/icons/Notifications';
import Checkbox from '@material-ui/core/Checkbox';
import Check from '@material-ui/icons/Check';
import Bookmark from '@material-ui/icons/Bookmark';
const useStyles = makeStyles(theme => ({
notificationRow: {
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.linkHover,
},
},
checkBox: {
padding: '0 10px 10px 0',
},
}));
/** @public */
export const NotificationsTable = (props: {
notifications?: Notification[];
}) => {
const { notifications } = props;
const navigate = useNavigate();
const styles = useStyles();
// TODO: Add select all
// TODO: Make mark as read work
// TODO: Check status of the notification and change to "Mark as unread" if it's already read
// TODO: Add support to save notifications (storageApi)
// TODO: Show timestamp relative time (react-relative-time npm package)
// TODO: Add signals listener and refresh data on message
// TODO: Handle no notifications properly
// TODO: Handle loading notifications
return (
<Table size="small">
<TableHead>
<TableRow>
<TableCell colSpan={3}>
{notifications?.length ?? 0} notifications
</TableCell>
</TableRow>
</TableHead>
{props.notifications?.map(notification => {
return (
<TableRow key={notification.id} className={styles.notificationRow}>
<TableCell width={100} style={{ verticalAlign: 'center' }}>
<Checkbox className={styles.checkBox} size="small" />
{notification.icon ?? <NotificationsIcon fontSize="small" />}
</TableCell>
<TableCell onClick={() => navigate(notification.link)}>
<Typography variant="subtitle2">{notification.title}</Typography>
<Typography variant="body2">
{notification.description}
</Typography>
</TableCell>
<TableCell style={{ textAlign: 'right' }}>
<Tooltip title="Mark as read">
<IconButton>
<Check fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Save">
<IconButton>
<Bookmark fontSize="small" />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
);
})}
</Table>
);
};
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './NotificationsTable';
@@ -0,0 +1,17 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './NotificationsSideBarItem';
export * from './NotificationsTable';
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './useNotificationsApi';
@@ -0,0 +1,31 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { NotificationsApi, notificationsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import useAsyncRetry from 'react-use/lib/useAsyncRetry';
/** @public */
export function useNotificationsApi<T>(
f: (api: NotificationsApi) => Promise<T>,
deps: any[] = [],
) {
const notificationsApi = useApi(notificationsApiRef);
return useAsyncRetry(async () => {
return await f(notificationsApi);
}, deps);
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { notificationsPlugin, NotificationsPage } from './plugin';
export * from './api';
export * from './hooks';
export * from './components';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { notificationsPlugin } from './plugin';
describe('notifications', () => {
it('should export plugin', () => {
expect(notificationsPlugin).toBeDefined();
});
});
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
createApiFactory,
createPlugin,
createRoutableExtension,
discoveryApiRef,
fetchApiRef,
} from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
import { notificationsApiRef } from './api/NotificationsApi';
import { NotificationsClient } from './api';
/** @public */
export const notificationsPlugin = createPlugin({
id: 'notifications',
routes: {
root: rootRouteRef,
},
apis: [
createApiFactory({
api: notificationsApiRef,
deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
factory: ({ discoveryApi, fetchApi }) =>
new NotificationsClient({ discoveryApi, fetchApi }),
}),
],
});
/** @public */
export const NotificationsPage = notificationsPlugin.provide(
createRoutableExtension({
name: 'NotificationsPage',
component: () =>
import('./components/NotificationsPage').then(m => m.NotificationsPage),
mountPoint: rootRouteRef,
}),
);
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'notifications',
});
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '@testing-library/jest-dom';
+129 -1
View File
@@ -12,7 +12,7 @@ __metadata:
languageName: node
linkType: hard
"@adobe/css-tools@npm:^4.3.2":
"@adobe/css-tools@npm:^4.0.1, @adobe/css-tools@npm:^4.3.2":
version: 4.3.2
resolution: "@adobe/css-tools@npm:4.3.2"
checksum: 9667d61d55dc3b0a315c530ae84e016ce5267c4dd8ac00abb40108dc98e07b98e3090ce8b87acd51a41a68d9e84dcccb08cdf21c902572a9cf9dcaf830da4ae3
@@ -7779,6 +7779,77 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-notifications-backend@^0.0.0, @backstage/plugin-notifications-backend@workspace:plugins/notifications-backend":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications-backend@workspace:plugins/notifications-backend"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
"@backstage/plugin-notifications-node": "workspace:^"
"@types/express": "*"
"@types/supertest": ^2.0.8
express: ^4.17.1
express-promise-router: ^4.1.0
knex: ^3.0.0
msw: ^1.0.0
node-fetch: ^2.6.7
supertest: ^6.2.4
winston: ^3.2.1
yn: ^4.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-notifications-common@workspace:^, @backstage/plugin-notifications-common@workspace:plugins/notifications-common":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications-common@workspace:plugins/notifications-common"
dependencies:
"@backstage/cli": "workspace:^"
languageName: unknown
linkType: soft
"@backstage/plugin-notifications-node@workspace:^, @backstage/plugin-notifications-node@workspace:plugins/notifications-node":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications-node@workspace:plugins/notifications-node"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/catalog-client": "workspace:^"
"@backstage/catalog-model": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
knex: ^3.0.0
uuid: ^8.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-notifications@^0.0.0, @backstage/plugin-notifications@workspace:plugins/notifications":
version: 0.0.0-use.local
resolution: "@backstage/plugin-notifications@workspace:plugins/notifications"
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@material-ui/core": ^4.9.13
"@material-ui/icons": ^4.9.1
"@material-ui/lab": ^4.0.0-alpha.61
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@testing-library/user-event": ^14.0.0
msw: ^1.0.0
react-use: ^17.2.4
peerDependencies:
react: ^16.13.1 || ^17.0.0
react-router-dom: 6.0.0-beta.0 || ^6.3.0
languageName: unknown
linkType: soft
"@backstage/plugin-octopus-deploy@workspace:^, @backstage/plugin-octopus-deploy@workspace:plugins/octopus-deploy":
version: 0.0.0-use.local
resolution: "@backstage/plugin-octopus-deploy@workspace:plugins/octopus-deploy"
@@ -17586,6 +17657,22 @@ __metadata:
languageName: unknown
linkType: soft
"@testing-library/dom@npm:^8.0.0":
version: 8.20.1
resolution: "@testing-library/dom@npm:8.20.1"
dependencies:
"@babel/code-frame": ^7.10.4
"@babel/runtime": ^7.12.5
"@types/aria-query": ^5.0.1
aria-query: 5.1.3
chalk: ^4.1.0
dom-accessibility-api: ^0.5.9
lz-string: ^1.5.0
pretty-format: ^27.0.2
checksum: 06fc8dc67849aadb726cbbad0e7546afdf8923bd39acb64c576d706249bd7d0d05f08e08a31913fb621162e3b9c2bd0dce15964437f030f9fa4476326fdd3007
languageName: node
linkType: hard
"@testing-library/dom@npm:^9.0.0":
version: 9.3.4
resolution: "@testing-library/dom@npm:9.3.4"
@@ -17602,6 +17689,23 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/jest-dom@npm:^5.10.1":
version: 5.17.0
resolution: "@testing-library/jest-dom@npm:5.17.0"
dependencies:
"@adobe/css-tools": ^4.0.1
"@babel/runtime": ^7.9.2
"@types/testing-library__jest-dom": ^5.9.1
aria-query: ^5.0.0
chalk: ^3.0.0
css.escape: ^1.5.1
dom-accessibility-api: ^0.5.6
lodash: ^4.17.15
redent: ^3.0.0
checksum: 9f28dbca8b50d7c306aae40c3aa8e06f0e115f740360004bd87d57f95acf7ab4b4f4122a7399a76dbf2bdaaafb15c99cc137fdcb0ae457a92e2de0f3fbf9b03b
languageName: node
linkType: hard
"@testing-library/jest-dom@npm:^6.0.0":
version: 6.3.0
resolution: "@testing-library/jest-dom@npm:6.3.0"
@@ -17657,6 +17761,20 @@ __metadata:
languageName: node
linkType: hard
"@testing-library/react@npm:^12.1.3":
version: 12.1.5
resolution: "@testing-library/react@npm:12.1.5"
dependencies:
"@babel/runtime": ^7.12.5
"@testing-library/dom": ^8.0.0
"@types/react-dom": <18.0.0
peerDependencies:
react: <18.0.0
react-dom: <18.0.0
checksum: 4abd0490405e709a7df584a0db604e508a4612398bb1326e8fa32dd9393b15badc826dcf6d2f7525437886d507871f719f127b9860ed69ddd204d1fa834f576a
languageName: node
linkType: hard
"@testing-library/react@npm:^14.0.0":
version: 14.2.1
resolution: "@testing-library/react@npm:14.2.1"
@@ -25191,6 +25309,13 @@ __metadata:
languageName: node
linkType: hard
"dom-accessibility-api@npm:^0.5.6":
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"
@@ -27007,6 +27132,7 @@ __metadata:
"@backstage/plugin-newrelic": "workspace:^"
"@backstage/plugin-newrelic-dashboard": "workspace:^"
"@backstage/plugin-nomad": "workspace:^"
"@backstage/plugin-notifications": ^0.0.0
"@backstage/plugin-octopus-deploy": "workspace:^"
"@backstage/plugin-org": "workspace:^"
"@backstage/plugin-pagerduty": "workspace:^"
@@ -27144,6 +27270,8 @@ __metadata:
"@backstage/plugin-lighthouse-backend": "workspace:^"
"@backstage/plugin-linguist-backend": "workspace:^"
"@backstage/plugin-nomad-backend": "workspace:^"
"@backstage/plugin-notifications-backend": ^0.0.0
"@backstage/plugin-notifications-node": "workspace:^"
"@backstage/plugin-permission-backend": "workspace:^"
"@backstage/plugin-permission-common": "workspace:^"
"@backstage/plugin-permission-node": "workspace:^"