From f24a0c1f6a784bc164f78b3d7bb5246f7b90d6ce Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Fri, 15 Dec 2023 14:05:35 +0200 Subject: [PATCH] feat: initial notifications support Signed-off-by: Heikki Hellgren --- packages/app/package.json | 1 + packages/app/src/App.tsx | 6 +- packages/app/src/components/Root/Root.tsx | 7 +- packages/backend/package.json | 2 + packages/backend/src/index.ts | 11 ++ packages/backend/src/plugins/notifications.ts | 39 +++++ packages/backend/src/types.ts | 6 +- plugins/notifications-backend/.eslintrc.js | 1 + plugins/notifications-backend/README.md | 14 ++ plugins/notifications-backend/api-report.md | 25 ++++ plugins/notifications-backend/package.json | 46 ++++++ plugins/notifications-backend/src/index.ts | 16 +++ plugins/notifications-backend/src/run.ts | 32 +++++ .../src/service/router.test.ts | 66 +++++++++ .../src/service/router.ts | 67 +++++++++ .../src/service/standaloneServer.ts | 99 +++++++++++++ .../notifications-backend/src/setupTests.ts | 16 +++ plugins/notifications-common/.eslintrc.js | 1 + plugins/notifications-common/README.md | 5 + plugins/notifications-common/api-report.md | 24 ++++ plugins/notifications-common/package.json | 32 +++++ plugins/notifications-common/src/index.ts | 23 +++ .../notifications-common/src/setupTests.ts | 16 +++ plugins/notifications-common/src/types.ts | 34 +++++ plugins/notifications-node/.eslintrc.js | 1 + plugins/notifications-node/README.md | 5 + plugins/notifications-node/api-report.md | 70 +++++++++ .../migrations/20231215_init.js | 34 +++++ plugins/notifications-node/package.json | 38 +++++ .../database/DatabaseNotificationsStore.ts | 97 +++++++++++++ .../src/database/NotificationsStore.ts | 36 +++++ .../notifications-node/src/database/index.ts | 17 +++ plugins/notifications-node/src/index.ts | 24 ++++ .../src/service/NotificationService.ts | 136 ++++++++++++++++++ .../notifications-node/src/service/index.ts | 16 +++ plugins/notifications-node/src/setupTests.ts | 16 +++ plugins/notifications/.eslintrc.js | 1 + plugins/notifications/README.md | 13 ++ plugins/notifications/api-report.md | 88 ++++++++++++ plugins/notifications/dev/index.tsx | 27 ++++ plugins/notifications/package.json | 53 +++++++ .../notifications/src/api/NotificationsApi.ts | 34 +++++ .../src/api/NotificationsClient.ts | 57 ++++++++ plugins/notifications/src/api/index.ts | 17 +++ .../NotificationsPage/NotificationsPage.tsx | 82 +++++++++++ .../src/components/NotificationsPage/index.ts | 16 +++ .../NotificationsSideBarItem.tsx | 49 +++++++ .../NotificationsSideBarItem/index.ts | 16 +++ .../NotificationsTable/NotificationsTable.tsx | 100 +++++++++++++ .../components/NotificationsTable/index.ts | 16 +++ plugins/notifications/src/components/index.ts | 17 +++ plugins/notifications/src/hooks/index.ts | 16 +++ .../src/hooks/useNotificationsApi.ts | 31 ++++ plugins/notifications/src/index.ts | 19 +++ plugins/notifications/src/plugin.test.ts | 22 +++ plugins/notifications/src/plugin.ts | 52 +++++++ plugins/notifications/src/routes.ts | 20 +++ plugins/notifications/src/setupTests.ts | 16 +++ yarn.lock | 130 ++++++++++++++++- 59 files changed, 1964 insertions(+), 7 deletions(-) create mode 100644 packages/backend/src/plugins/notifications.ts create mode 100644 plugins/notifications-backend/.eslintrc.js create mode 100644 plugins/notifications-backend/README.md create mode 100644 plugins/notifications-backend/api-report.md create mode 100644 plugins/notifications-backend/package.json create mode 100644 plugins/notifications-backend/src/index.ts create mode 100644 plugins/notifications-backend/src/run.ts create mode 100644 plugins/notifications-backend/src/service/router.test.ts create mode 100644 plugins/notifications-backend/src/service/router.ts create mode 100644 plugins/notifications-backend/src/service/standaloneServer.ts create mode 100644 plugins/notifications-backend/src/setupTests.ts create mode 100644 plugins/notifications-common/.eslintrc.js create mode 100644 plugins/notifications-common/README.md create mode 100644 plugins/notifications-common/api-report.md create mode 100644 plugins/notifications-common/package.json create mode 100644 plugins/notifications-common/src/index.ts create mode 100644 plugins/notifications-common/src/setupTests.ts create mode 100644 plugins/notifications-common/src/types.ts create mode 100644 plugins/notifications-node/.eslintrc.js create mode 100644 plugins/notifications-node/README.md create mode 100644 plugins/notifications-node/api-report.md create mode 100644 plugins/notifications-node/migrations/20231215_init.js create mode 100644 plugins/notifications-node/package.json create mode 100644 plugins/notifications-node/src/database/DatabaseNotificationsStore.ts create mode 100644 plugins/notifications-node/src/database/NotificationsStore.ts create mode 100644 plugins/notifications-node/src/database/index.ts create mode 100644 plugins/notifications-node/src/index.ts create mode 100644 plugins/notifications-node/src/service/NotificationService.ts create mode 100644 plugins/notifications-node/src/service/index.ts create mode 100644 plugins/notifications-node/src/setupTests.ts create mode 100644 plugins/notifications/.eslintrc.js create mode 100644 plugins/notifications/README.md create mode 100644 plugins/notifications/api-report.md create mode 100644 plugins/notifications/dev/index.tsx create mode 100644 plugins/notifications/package.json create mode 100644 plugins/notifications/src/api/NotificationsApi.ts create mode 100644 plugins/notifications/src/api/NotificationsClient.ts create mode 100644 plugins/notifications/src/api/index.ts create mode 100644 plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx create mode 100644 plugins/notifications/src/components/NotificationsPage/index.ts create mode 100644 plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx create mode 100644 plugins/notifications/src/components/NotificationsSideBarItem/index.ts create mode 100644 plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx create mode 100644 plugins/notifications/src/components/NotificationsTable/index.ts create mode 100644 plugins/notifications/src/components/index.ts create mode 100644 plugins/notifications/src/hooks/index.ts create mode 100644 plugins/notifications/src/hooks/useNotificationsApi.ts create mode 100644 plugins/notifications/src/index.ts create mode 100644 plugins/notifications/src/plugin.test.ts create mode 100644 plugins/notifications/src/plugin.ts create mode 100644 plugins/notifications/src/routes.ts create mode 100644 plugins/notifications/src/setupTests.ts 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 ( + + + + + + + + + + + + + + + + + ); +}; 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 ( + + + + + {notifications?.length ?? 0} notifications + + + + {props.notifications?.map(notification => { + return ( + + + + {notification.icon ?? } + + navigate(notification.link)}> + {notification.title} + + {notification.description} + + + + + + + + + + + + + + + + ); + })} +
+ ); +}; 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:^"