diff --git a/.changeset/light-eggs-switch.md b/.changeset/light-eggs-switch.md
new file mode 100644
index 0000000000..3fbb77b0d8
--- /dev/null
+++ b/.changeset/light-eggs-switch.md
@@ -0,0 +1,5 @@
+---
+'@backstage/dev-utils': patch
+---
+
+Allow defining custom side bar item for page
diff --git a/.changeset/rude-hats-bow.md b/.changeset/rude-hats-bow.md
new file mode 100644
index 0000000000..0bd84617c0
--- /dev/null
+++ b/.changeset/rude-hats-bow.md
@@ -0,0 +1,8 @@
+---
+'@backstage/plugin-notifications-backend': patch
+'@backstage/plugin-signals-backend': patch
+'@backstage/plugin-notifications': patch
+'@backstage/plugin-signals': patch
+---
+
+Migrate signals and notifications to the new backend in local development
diff --git a/packages/dev-utils/api-report.md b/packages/dev-utils/api-report.md
index 3f21ba5502..c44f7328b9 100644
--- a/packages/dev-utils/api-report.md
+++ b/packages/dev-utils/api-report.md
@@ -14,6 +14,7 @@ import { GridProps } from '@material-ui/core/Grid';
import { IconComponent } from '@backstage/core-plugin-api';
import { PropsWithChildren } from 'react';
import { ReactNode } from 'react';
+import { RouteRef } from '@backstage/core-plugin-api';
// @public
export function createDevApp(): DevAppBuilder;
@@ -42,6 +43,8 @@ export type DevAppPageOptions = {
children?: JSX.Element;
title?: string;
icon?: IconComponent;
+ sideBarItem?: JSX.Element;
+ routeRef?: RouteRef;
};
// @public (undocumented)
diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx
index 79834cddc8..1b57a4b9b7 100644
--- a/packages/dev-utils/src/devApp/render.tsx
+++ b/packages/dev-utils/src/devApp/render.tsx
@@ -44,7 +44,7 @@ import {
} from '@backstage/integration-react';
import Box from '@material-ui/core/Box';
import BookmarkIcon from '@material-ui/icons/Bookmark';
-import React, { ComponentType, ReactNode, PropsWithChildren } from 'react';
+import React, { ComponentType, PropsWithChildren, ReactNode } from 'react';
import { createRoutesFromChildren, Route } from 'react-router-dom';
import { SidebarThemeSwitcher } from './SidebarThemeSwitcher';
import 'react-dom';
@@ -80,6 +80,9 @@ export type DevAppPageOptions = {
children?: JSX.Element;
title?: string;
icon?: IconComponent;
+ sideBarItem?: JSX.Element;
+
+ routeRef?: RouteRef;
};
/**
@@ -141,7 +144,9 @@ export class DevAppBuilder {
this.defaultPage = path;
}
- if (opts.title) {
+ if (opts.sideBarItem) {
+ this.sidebarItems.push(opts.sideBarItem);
+ } else if (opts.title) {
this.sidebarItems.push(
,
);
}
- this.routes.push(
- ,
- );
+
+ if (opts.routeRef) {
+ const Elem = () => <>{opts.element}>;
+ attachComponentData(Elem, 'core.mountPoint', opts.routeRef);
+
+ this.routes.push(
+ }
+ children={opts.children}
+ />,
+ );
+ } else {
+ this.routes.push(
+ ,
+ );
+ }
return this;
}
diff --git a/plugins/notifications-backend/dev/index.ts b/plugins/notifications-backend/dev/index.ts
new file mode 100644
index 0000000000..987f984dcb
--- /dev/null
+++ b/plugins/notifications-backend/dev/index.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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 { createBackend } from '@backstage/backend-defaults';
+import {
+ coreServices,
+ createBackendPlugin,
+} from '@backstage/backend-plugin-api';
+import { notificationService } from '@backstage/plugin-notifications-node';
+
+const notificationsDebug = createBackendPlugin({
+ pluginId: 'notifications-debug',
+ register(env) {
+ env.registerInit({
+ deps: {
+ notifications: notificationService,
+ lifecycle: coreServices.lifecycle,
+ },
+ async init({ notifications, lifecycle }) {
+ let interval: NodeJS.Timeout | undefined;
+ lifecycle.addStartupHook(async () => {
+ interval = setInterval(async () => {
+ await notifications.send({
+ recipients: {
+ type: 'entity',
+ entityRef: 'user:development/guest',
+ },
+ payload: { title: 'Test notification' },
+ });
+ }, 5000);
+ });
+
+ lifecycle.addShutdownHook(async () => {
+ if (interval) {
+ clearInterval(interval);
+ }
+ });
+ },
+ });
+ },
+});
+
+const backend = createBackend();
+backend.add(import('@backstage/plugin-events-backend/alpha'));
+backend.add(import('@backstage/plugin-signals-backend'));
+backend.add(import('@backstage/plugin-auth-backend'));
+backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
+backend.add(import('../src'));
+backend.add(notificationsDebug);
+
+backend.start();
diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json
index 15297856fb..cdb7642301 100644
--- a/plugins/notifications-backend/package.json
+++ b/plugins/notifications-backend/package.json
@@ -51,8 +51,13 @@
"yn": "^4.0.0"
},
"devDependencies": {
+ "@backstage/backend-defaults": "workspace:^",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
+ "@backstage/plugin-auth-backend": "workspace:^",
+ "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^",
+ "@backstage/plugin-events-backend": "workspace:^",
+ "@backstage/plugin-signals-backend": "workspace:^",
"@types/express": "^4.17.6",
"@types/supertest": "^2.0.8",
"msw": "^1.0.0",
diff --git a/plugins/notifications-backend/src/run.ts b/plugins/notifications-backend/src/run.ts
deleted file mode 100644
index d299ed23e9..0000000000
--- a/plugins/notifications-backend/src/run.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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/standaloneServer.ts b/plugins/notifications-backend/src/service/standaloneServer.ts
deleted file mode 100644
index 5445d9b6b4..0000000000
--- a/plugins/notifications-backend/src/service/standaloneServer.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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 {
- createLegacyAuthAdapters,
- createServiceBuilder,
- HostDiscovery,
- loadBackendConfig,
- PluginDatabaseManager,
- ServerTokenManager,
-} 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 {
- CatalogApi,
- CatalogRequestOptions,
- GetEntitiesByRefsRequest,
-} from '@backstage/catalog-client';
-import { DefaultSignalsService } from '@backstage/plugin-signals-node';
-import {
- EventParams,
- EventsService,
- EventsServiceSubscribeOptions,
-} from '@backstage/plugin-events-node';
-import {
- AuthService,
- HttpAuthService,
- UserInfoService,
-} from '@backstage/backend-plugin-api';
-
-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 tokenManager = ServerTokenManager.fromConfig(config, {
- logger,
- });
- const discovery = HostDiscovery.fromConfig(config);
-
- const dbMock: PluginDatabaseManager = {
- async getClient() {
- return db;
- },
- };
-
- const catalogApi = {
- async getEntitiesByRefs(
- _request: GetEntitiesByRefsRequest,
- __options?: CatalogRequestOptions,
- ) {
- return {
- items: [
- {
- apiVersion: 'backstage.io/v1alpha1',
- kind: 'User',
- metadata: { name: 'user', namespace: 'default' },
- spec: {},
- },
- ],
- };
- },
- } as Partial as CatalogApi;
-
- 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 mockSubscribers: EventsServiceSubscribeOptions[] = [];
- const events: EventsService = {
- async publish(params: EventParams): Promise {
- mockSubscribers.forEach(sub => sub.onEvent(params));
- },
- async subscribe(subscription: EventsServiceSubscribeOptions) {
- mockSubscribers.push(subscription);
- },
- };
-
- const signalService = DefaultSignalsService.create({ events });
- // TODO: Move to use services instead this hack
- const { auth, httpAuth, userInfo } = createLegacyAuthAdapters<
- any,
- { auth: AuthService; httpAuth: HttpAuthService; userInfo: UserInfoService }
- >({
- identity: identityMock,
- tokenManager,
- discovery,
- });
-
- const router = await createRouter({
- logger,
- database: dbMock,
- catalog: catalogApi,
- discovery,
- signals: signalService,
- auth,
- httpAuth,
- userInfo,
- });
-
- 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/dev/index.tsx b/plugins/notifications/dev/index.tsx
index f4ea31f4ab..27bfe23842 100644
--- a/plugins/notifications/dev/index.tsx
+++ b/plugins/notifications/dev/index.tsx
@@ -16,12 +16,17 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { notificationsPlugin } from '../src/plugin';
+import { NotificationsPage } from '../src/components/NotificationsPage';
+import { NotificationsSidebarItem } from '../src';
+import { rootRouteRef } from '../src/routes';
+// TODO: How to sign in here as guest user?
createDevApp()
.registerPlugin(notificationsPlugin)
.addPage({
- element: ,
- title: 'Root Page',
+ element: ,
path: '/notifications',
+ sideBarItem: ,
+ routeRef: rootRouteRef,
})
.render();
diff --git a/plugins/signals-backend/dev/index.ts b/plugins/signals-backend/dev/index.ts
new file mode 100644
index 0000000000..ae98e54eed
--- /dev/null
+++ b/plugins/signals-backend/dev/index.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024 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 { createBackend } from '@backstage/backend-defaults';
+import {
+ coreServices,
+ createBackendPlugin,
+} from '@backstage/backend-plugin-api';
+import { signalService } from '@backstage/plugin-signals-node';
+
+const signalDebug = createBackendPlugin({
+ pluginId: 'signals-debug',
+ register(env) {
+ env.registerInit({
+ deps: {
+ signals: signalService,
+ lifecycle: coreServices.lifecycle,
+ },
+ async init({ signals, lifecycle }) {
+ let interval: NodeJS.Timeout | undefined;
+ lifecycle.addStartupHook(async () => {
+ interval = setInterval(async () => {
+ await signals.publish<{ date: string }>({
+ channel: 'debug',
+ message: { date: new Date().toISOString() },
+ recipients: { type: 'broadcast' },
+ });
+ }, 1000);
+ });
+
+ lifecycle.addShutdownHook(async () => {
+ if (interval) {
+ clearInterval(interval);
+ }
+ });
+ },
+ });
+ },
+});
+
+const backend = createBackend();
+backend.add(import('@backstage/plugin-events-backend/alpha'));
+backend.add(import('../src'));
+backend.add(signalDebug);
+
+backend.start();
diff --git a/plugins/signals-backend/package.json b/plugins/signals-backend/package.json
index 24682829e8..923b4bb150 100644
--- a/plugins/signals-backend/package.json
+++ b/plugins/signals-backend/package.json
@@ -45,7 +45,9 @@
"yn": "^4.0.0"
},
"devDependencies": {
+ "@backstage/backend-defaults": "workspace:^",
"@backstage/cli": "workspace:^",
+ "@backstage/plugin-events-backend": "workspace:^",
"@types/supertest": "^2.0.8",
"msw": "^1.0.0",
"supertest": "^6.2.4"
diff --git a/plugins/signals-backend/src/run.ts b/plugins/signals-backend/src/run.ts
deleted file mode 100644
index d299ed23e9..0000000000
--- a/plugins/signals-backend/src/run.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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/signals-backend/src/service/standaloneServer.ts b/plugins/signals-backend/src/service/standaloneServer.ts
deleted file mode 100644
index efb7ecedee..0000000000
--- a/plugins/signals-backend/src/service/standaloneServer.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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,
- HostDiscovery,
- loadBackendConfig,
-} from '@backstage/backend-common';
-import { Server } from 'http';
-import { Logger } from 'winston';
-import { createRouter } from './router';
-import { DefaultSignalsService } from '@backstage/plugin-signals-node';
-import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
-import {
- EventParams,
- EventsService,
- EventsServiceSubscribeOptions,
-} from '@backstage/plugin-events-node';
-import {
- BackstageCredentials,
- BackstageUserInfo,
- UserInfoService,
-} from '@backstage/backend-plugin-api';
-
-export interface ServerOptions {
- port: number;
- enableCors: boolean;
- logger: Logger;
-}
-
-export async function startStandaloneServer(
- options: ServerOptions,
-): Promise {
- const logger = options.logger.child({ service: 'signals-backend' });
- logger.debug('Starting application server...');
- const config = await loadBackendConfig({ logger, argv: process.argv });
- const discovery = HostDiscovery.fromConfig(config);
-
- const identity = DefaultIdentityClient.create({
- discovery,
- issuer: await discovery.getExternalBaseUrl('auth'),
- });
-
- const mockSubscribers: EventsServiceSubscribeOptions[] = [];
- const events: EventsService = {
- async publish(params: EventParams): Promise {
- mockSubscribers.forEach(sub => sub.onEvent(params));
- },
- async subscribe(subscription: EventsServiceSubscribeOptions) {
- mockSubscribers.push(subscription);
- },
- };
-
- const signals = DefaultSignalsService.create({
- events,
- });
-
- const userInfo: UserInfoService = {
- async getUserInfo(_: BackstageCredentials): Promise {
- return {
- userEntityRef: 'user:default/guest',
- ownershipEntityRefs: ['user:default/guest'],
- };
- },
- };
-
- const router = await createRouter({
- logger,
- identity,
- events,
- discovery,
- userInfo,
- });
-
- let service = createServiceBuilder(module)
- .setPort(options.port)
- .addRouter('/signals', router);
- if (options.enableCors) {
- service = service.enableCors({ origin: 'http://localhost:3000' });
- }
-
- let server: Promise;
- try {
- server = service.start();
-
- setInterval(() => {
- signals.publish({
- recipients: { type: 'broadcast' },
- channel: 'test',
- message: { hello: 'world' },
- });
- }, 5000);
- } catch (err) {
- logger.error(err);
- process.exit(1);
- }
- return server;
-}
-
-module.hot?.accept();
diff --git a/plugins/signals/dev/index.tsx b/plugins/signals/dev/index.tsx
index 2b22424eee..629c820a8e 100644
--- a/plugins/signals/dev/index.tsx
+++ b/plugins/signals/dev/index.tsx
@@ -16,20 +16,33 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
import { signalsPlugin } from '../src/plugin';
-import { Content, Header, Page } from '@backstage/core-components';
+import { CodeSnippet, Content, Header, Page } from '@backstage/core-components';
import Typography from '@material-ui/core/Typography';
+import { useSignal } from '@backstage/plugin-signals-react';
+
+const SignalsDebugPage = () => {
+ const { lastSignal } = useSignal('debug');
+ return (
+
+
+
+ Last signal:
+
+ {lastSignal ? (
+
+ ) : (
+ 'Not received'
+ )}
+
+
+
+ );
+};
createDevApp()
.registerPlugin(signalsPlugin)
.addPage({
title: 'Debug',
- element: (
-
-
-
- TODO
-
-
- ),
+ element: ,
})
.render();
diff --git a/yarn.lock b/yarn.lock
index bff13e8dd1..e392282e15 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7899,6 +7899,7 @@ __metadata:
resolution: "@backstage/plugin-notifications-backend@workspace:plugins/notifications-backend"
dependencies:
"@backstage/backend-common": "workspace:^"
+ "@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/catalog-client": "workspace:^"
@@ -7906,10 +7907,14 @@ __metadata:
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
+ "@backstage/plugin-auth-backend": "workspace:^"
+ "@backstage/plugin-auth-backend-module-guest-provider": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
+ "@backstage/plugin-events-backend": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
"@backstage/plugin-notifications-common": "workspace:^"
"@backstage/plugin-notifications-node": "workspace:^"
+ "@backstage/plugin-signals-backend": "workspace:^"
"@backstage/plugin-signals-node": "workspace:^"
"@types/express": ^4.17.6
"@types/supertest": ^2.0.8
@@ -9276,10 +9281,12 @@ __metadata:
resolution: "@backstage/plugin-signals-backend@workspace:plugins/signals-backend"
dependencies:
"@backstage/backend-common": "workspace:^"
+ "@backstage/backend-defaults": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
+ "@backstage/plugin-events-backend": "workspace:^"
"@backstage/plugin-events-node": "workspace:^"
"@backstage/plugin-signals-node": "workspace:^"
"@backstage/types": "workspace:^"