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:^"