diff --git a/packages/app/package.json b/packages/app/package.json
index 31be0e1610..b960712550 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -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:^",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 90190b50be..3d8bd45e5a 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -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 = (
}>
{customDevToolsPage}
+ } />
);
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 5f11218bba..6294aa7856 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -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<{}>) => (
+
+
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 2386d799a9..1606158a7e 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -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:^",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index bade028597..fd29c4a8c7 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -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);
diff --git a/packages/backend/src/plugins/notifications.ts b/packages/backend/src/plugins/notifications.ts
new file mode 100644
index 0000000000..91c01fb860
--- /dev/null
+++ b/packages/backend/src/plugins/notifications.ts
@@ -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 {
+ // 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,
+ });
+}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 3dad2f739b..6a48804d1c 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -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;
};
diff --git a/plugins/notifications-backend/.eslintrc.js b/plugins/notifications-backend/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/notifications-backend/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/notifications-backend/README.md b/plugins/notifications-backend/README.md
new file mode 100644
index 0000000000..e90d287049
--- /dev/null
+++ b/plugins/notifications-backend/README.md
@@ -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.
diff --git a/plugins/notifications-backend/api-report.md b/plugins/notifications-backend/api-report.md
new file mode 100644
index 0000000000..e571fd521a
--- /dev/null
+++ b/plugins/notifications-backend/api-report.md
@@ -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;
+
+// @public (undocumented)
+export interface RouterOptions {
+ // (undocumented)
+ identity: IdentityApi;
+ // (undocumented)
+ logger: Logger;
+ // (undocumented)
+ notificationService: NotificationService;
+}
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json
new file mode 100644
index 0000000000..ce67513669
--- /dev/null
+++ b/plugins/notifications-backend/package.json
@@ -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"
+ ]
+}
diff --git a/plugins/notifications-backend/src/index.ts b/plugins/notifications-backend/src/index.ts
new file mode 100644
index 0000000000..d2e8d61bad
--- /dev/null
+++ b/plugins/notifications-backend/src/index.ts
@@ -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';
diff --git a/plugins/notifications-backend/src/run.ts b/plugins/notifications-backend/src/run.ts
new file mode 100644
index 0000000000..d299ed23e9
--- /dev/null
+++ b/plugins/notifications-backend/src/run.ts
@@ -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);
+});
diff --git a/plugins/notifications-backend/src/service/router.test.ts b/plugins/notifications-backend/src/service/router.test.ts
new file mode 100644
index 0000000000..c0f09b3ef9
--- /dev/null
+++ b/plugins/notifications-backend/src/service/router.test.ts
@@ -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> = {
+ 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' });
+ });
+ });
+});
diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts
new file mode 100644
index 0000000000..8d9a7472bb
--- /dev/null
+++ b/plugins/notifications-backend/src/service/router.ts
@@ -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 {
+ const { logger, notificationService, identity } = options;
+
+ const store = await notificationService.getStore();
+
+ const getUser = async (req: Request) => {
+ 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;
+}
diff --git a/plugins/notifications-backend/src/service/standaloneServer.ts b/plugins/notifications-backend/src/service/standaloneServer.ts
new file mode 100644
index 0000000000..c19ae01b74
--- /dev/null
+++ b/plugins/notifications-backend/src/service/standaloneServer.ts
@@ -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 {
+ 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 }) {
+ 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();
diff --git a/plugins/notifications-backend/src/setupTests.ts b/plugins/notifications-backend/src/setupTests.ts
new file mode 100644
index 0000000000..4b9026cde5
--- /dev/null
+++ b/plugins/notifications-backend/src/setupTests.ts
@@ -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 {};
diff --git a/plugins/notifications-common/.eslintrc.js b/plugins/notifications-common/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/notifications-common/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/notifications-common/README.md b/plugins/notifications-common/README.md
new file mode 100644
index 0000000000..673e30288f
--- /dev/null
+++ b/plugins/notifications-common/README.md
@@ -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_
diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md
new file mode 100644
index 0000000000..6d7d74b64b
--- /dev/null
+++ b/plugins/notifications-common/api-report.md
@@ -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;
+};
+```
diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json
new file mode 100644
index 0000000000..91acf171f7
--- /dev/null
+++ b/plugins/notifications-common/package.json
@@ -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"
+ ]
+}
diff --git a/plugins/notifications-common/src/index.ts b/plugins/notifications-common/src/index.ts
new file mode 100644
index 0000000000..8d9f26f07a
--- /dev/null
+++ b/plugins/notifications-common/src/index.ts
@@ -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';
diff --git a/plugins/notifications-common/src/setupTests.ts b/plugins/notifications-common/src/setupTests.ts
new file mode 100644
index 0000000000..4b9026cde5
--- /dev/null
+++ b/plugins/notifications-common/src/setupTests.ts
@@ -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 {};
diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts
new file mode 100644
index 0000000000..c9df78e1ff
--- /dev/null
+++ b/plugins/notifications-common/src/types.ts
@@ -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;
+};
diff --git a/plugins/notifications-node/.eslintrc.js b/plugins/notifications-node/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/notifications-node/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/notifications-node/README.md b/plugins/notifications-node/README.md
new file mode 100644
index 0000000000..4cf460c3fe
--- /dev/null
+++ b/plugins/notifications-node/README.md
@@ -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_
diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md
new file mode 100644
index 0000000000..68a53cd535
--- /dev/null
+++ b/plugins/notifications-node/api-report.md
@@ -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;
+ // (undocumented)
+ getNotifications(options: NotificationGetOptions): Promise;
+ // (undocumented)
+ getStatus(options: NotificationGetOptions): Promise<{
+ unread: number;
+ read: number;
+ }>;
+ // (undocumented)
+ saveNotification(notification: Notification_2): Promise;
+}
+
+// @public (undocumented)
+export type NotificationGetOptions = {
+ user_ref: string;
+};
+
+// @public (undocumented)
+export class NotificationService {
+ // (undocumented)
+ static create({
+ database,
+ discovery,
+ }: NotificationServiceOptions): NotificationService;
+ // (undocumented)
+ getStore(): Promise;
+ // (undocumented)
+ send(
+ entityRef: string | string[],
+ title: string,
+ description: string,
+ link: string,
+ ): Promise;
+}
+
+// @public (undocumented)
+export type NotificationServiceOptions = {
+ database: PluginDatabaseManager;
+ discovery: PluginEndpointDiscovery;
+};
+
+// @public (undocumented)
+export interface NotificationsStore {
+ // (undocumented)
+ getNotifications(options: NotificationGetOptions): Promise;
+ // (undocumented)
+ getStatus(options: NotificationGetOptions): Promise;
+ // (undocumented)
+ saveNotification(notification: Notification_2): Promise;
+}
+```
diff --git a/plugins/notifications-node/migrations/20231215_init.js b/plugins/notifications-node/migrations/20231215_init.js
new file mode 100644
index 0000000000..a76a8c3cbd
--- /dev/null
+++ b/plugins/notifications-node/migrations/20231215_init.js
@@ -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');
+};
diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json
new file mode 100644
index 0000000000..4a9e9a2319
--- /dev/null
+++ b/plugins/notifications-node/package.json
@@ -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"
+ }
+}
diff --git a/plugins/notifications-node/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-node/src/database/DatabaseNotificationsStore.ts
new file mode 100644
index 0000000000..450450d998
--- /dev/null
+++ b/plugins/notifications-node/src/database/DatabaseNotificationsStore.ts
@@ -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 {
+ 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),
+ };
+ }
+}
diff --git a/plugins/notifications-node/src/database/NotificationsStore.ts b/plugins/notifications-node/src/database/NotificationsStore.ts
new file mode 100644
index 0000000000..349e2cd7fa
--- /dev/null
+++ b/plugins/notifications-node/src/database/NotificationsStore.ts
@@ -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;
+
+ saveNotification(notification: Notification): Promise;
+
+ getStatus(options: NotificationGetOptions): Promise;
+
+ // TODO: Mark as read/unread by notification id(s)
+}
diff --git a/plugins/notifications-node/src/database/index.ts b/plugins/notifications-node/src/database/index.ts
new file mode 100644
index 0000000000..6d2f549f38
--- /dev/null
+++ b/plugins/notifications-node/src/database/index.ts
@@ -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';
diff --git a/plugins/notifications-node/src/index.ts b/plugins/notifications-node/src/index.ts
new file mode 100644
index 0000000000..c686e95d3c
--- /dev/null
+++ b/plugins/notifications-node/src/index.ts
@@ -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';
diff --git a/plugins/notifications-node/src/service/NotificationService.ts b/plugins/notifications-node/src/service/NotificationService.ts
new file mode 100644
index 0000000000..55dcff546f
--- /dev/null
+++ b/plugins/notifications-node/src/service/NotificationService.ts
@@ -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 {
+ 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 {
+ if (!this.store) {
+ this.store = await DatabaseNotificationsStore.create({
+ database: this.database,
+ });
+ }
+ return this.store;
+ }
+
+ private async getUsersForEntityRef(
+ entityRef: string | string[],
+ ): Promise {
+ const refs = Array.isArray(entityRef) ? entityRef : [entityRef];
+ const entities = await this.catalog.getEntitiesByRefs({ entityRefs: refs });
+
+ const mapEntity = async (entity: Entity | undefined): Promise => {
+ 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;
+ }
+}
diff --git a/plugins/notifications-node/src/service/index.ts b/plugins/notifications-node/src/service/index.ts
new file mode 100644
index 0000000000..450f4f7dcc
--- /dev/null
+++ b/plugins/notifications-node/src/service/index.ts
@@ -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';
diff --git a/plugins/notifications-node/src/setupTests.ts b/plugins/notifications-node/src/setupTests.ts
new file mode 100644
index 0000000000..4b9026cde5
--- /dev/null
+++ b/plugins/notifications-node/src/setupTests.ts
@@ -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 {};
diff --git a/plugins/notifications/.eslintrc.js b/plugins/notifications/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/notifications/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md
new file mode 100644
index 0000000000..3eeac7a647
--- /dev/null
+++ b/plugins/notifications/README.md
@@ -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.
diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md
new file mode 100644
index 0000000000..00ff5f4d4c
--- /dev/null
+++ b/plugins/notifications/api-report.md
@@ -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
+///
+
+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;
+ // (undocumented)
+ getStatus(): Promise;
+}
+
+// @public (undocumented)
+export const notificationsApiRef: ApiRef;
+
+// @public (undocumented)
+export class NotificationsClient implements NotificationsApi {
+ constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi });
+ // (undocumented)
+ getNotifications(): Promise;
+ // (undocumented)
+ getStatus(): Promise;
+}
+
+// @public (undocumented)
+export const NotificationsPage: () => JSX_2.Element;
+
+// @public (undocumented)
+export const notificationsPlugin: BackstagePlugin<
+ {
+ root: RouteRef;
+ },
+ {}
+>;
+
+// @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(
+ f: (api: NotificationsApi) => Promise,
+ 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)
+```
diff --git a/plugins/notifications/dev/index.tsx b/plugins/notifications/dev/index.tsx
new file mode 100644
index 0000000000..f4ea31f4ab
--- /dev/null
+++ b/plugins/notifications/dev/index.tsx
@@ -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: ,
+ title: 'Root Page',
+ path: '/notifications',
+ })
+ .render();
diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json
new file mode 100644
index 0000000000..9e78fc32de
--- /dev/null
+++ b/plugins/notifications/package.json
@@ -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"
+ ]
+}
diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts
new file mode 100644
index 0000000000..a4dcf3cd28
--- /dev/null
+++ b/plugins/notifications/src/api/NotificationsApi.ts
@@ -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({
+ id: 'plugin.notifications.service',
+});
+
+/** @public */
+export interface NotificationsApi {
+ getNotifications(): Promise;
+
+ getStatus(): Promise;
+
+ // TODO: Mark as read/unread by notification id(s)
+}
diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts
new file mode 100644
index 0000000000..4947900d55
--- /dev/null
+++ b/plugins/notifications/src/api/NotificationsClient.ts
@@ -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 {
+ return await this.get('notifications');
+ }
+
+ async getStatus(): Promise {
+ return await this.get('status');
+ }
+
+ private async get(path: string): Promise {
+ 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;
+ }
+}
diff --git a/plugins/notifications/src/api/index.ts b/plugins/notifications/src/api/index.ts
new file mode 100644
index 0000000000..bf39e313b8
--- /dev/null
+++ b/plugins/notifications/src/api/index.ts
@@ -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';
diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx
new file mode 100644
index 0000000000..e00b45df53
--- /dev/null
+++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx
@@ -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 ;
+ }
+
+ // TODO: Make the filter buttons work
+ // TODO: Add signals listener and refresh data on message
+ return (
+
+
+
+
+ }>
+ Inbox
+
+ }>
+ Done
+
+ }>
+ Saved
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/notifications/src/components/NotificationsPage/index.ts b/plugins/notifications/src/components/NotificationsPage/index.ts
new file mode 100644
index 0000000000..ace54c34d0
--- /dev/null
+++ b/plugins/notifications/src/components/NotificationsPage/index.ts
@@ -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';
diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx
new file mode 100644
index 0000000000..6a562b20c3
--- /dev/null
+++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx
@@ -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 (
+
+ );
+};
diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/index.ts b/plugins/notifications/src/components/NotificationsSideBarItem/index.ts
new file mode 100644
index 0000000000..22d6b5d881
--- /dev/null
+++ b/plugins/notifications/src/components/NotificationsSideBarItem/index.ts
@@ -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';
diff --git a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx
new file mode 100644
index 0000000000..6e990cf3ea
--- /dev/null
+++ b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx
@@ -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 (
+
+ );
+};
diff --git a/plugins/notifications/src/components/NotificationsTable/index.ts b/plugins/notifications/src/components/NotificationsTable/index.ts
new file mode 100644
index 0000000000..a1fb7a25c6
--- /dev/null
+++ b/plugins/notifications/src/components/NotificationsTable/index.ts
@@ -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';
diff --git a/plugins/notifications/src/components/index.ts b/plugins/notifications/src/components/index.ts
new file mode 100644
index 0000000000..3bca70a215
--- /dev/null
+++ b/plugins/notifications/src/components/index.ts
@@ -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';
diff --git a/plugins/notifications/src/hooks/index.ts b/plugins/notifications/src/hooks/index.ts
new file mode 100644
index 0000000000..2613596f72
--- /dev/null
+++ b/plugins/notifications/src/hooks/index.ts
@@ -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';
diff --git a/plugins/notifications/src/hooks/useNotificationsApi.ts b/plugins/notifications/src/hooks/useNotificationsApi.ts
new file mode 100644
index 0000000000..e1afb54c8d
--- /dev/null
+++ b/plugins/notifications/src/hooks/useNotificationsApi.ts
@@ -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(
+ f: (api: NotificationsApi) => Promise,
+ deps: any[] = [],
+) {
+ const notificationsApi = useApi(notificationsApiRef);
+
+ return useAsyncRetry(async () => {
+ return await f(notificationsApi);
+ }, deps);
+}
diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts
new file mode 100644
index 0000000000..bb5d76a0cf
--- /dev/null
+++ b/plugins/notifications/src/index.ts
@@ -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';
diff --git a/plugins/notifications/src/plugin.test.ts b/plugins/notifications/src/plugin.test.ts
new file mode 100644
index 0000000000..3bf749aba9
--- /dev/null
+++ b/plugins/notifications/src/plugin.test.ts
@@ -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();
+ });
+});
diff --git a/plugins/notifications/src/plugin.ts b/plugins/notifications/src/plugin.ts
new file mode 100644
index 0000000000..472e519183
--- /dev/null
+++ b/plugins/notifications/src/plugin.ts
@@ -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,
+ }),
+);
diff --git a/plugins/notifications/src/routes.ts b/plugins/notifications/src/routes.ts
new file mode 100644
index 0000000000..f307038701
--- /dev/null
+++ b/plugins/notifications/src/routes.ts
@@ -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',
+});
diff --git a/plugins/notifications/src/setupTests.ts b/plugins/notifications/src/setupTests.ts
new file mode 100644
index 0000000000..865308e634
--- /dev/null
+++ b/plugins/notifications/src/setupTests.ts
@@ -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';
diff --git a/yarn.lock b/yarn.lock
index 5f84cbbc51..23efe10941 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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:^"