From da5a35a38ce6ec7379c3b452b1782abd6b91b2bf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Feb 2026 14:11:04 +0100 Subject: [PATCH] frontend-plugin-api,app: move toast forwading to be internal Signed-off-by: Patrik Oldsberg --- packages/frontend-plugin-api/report.api.md | 15 ++--- .../src/apis/definitions/ToastApi.ts | 29 +-------- plugins/app/report.api.md | 15 +++++ plugins/app/src/apis/ToastApiForwarder.ts | 16 ++--- plugins/app/src/apis/index.ts | 2 + plugins/app/src/apis/toastApiForwarderRef.ts | 61 +++++++++++++++++++ .../components/Toast/ToastDisplay.test.tsx | 5 +- .../app/src/components/Toast/ToastDisplay.tsx | 4 +- plugins/app/src/defaultApis.ts | 15 ++++- 9 files changed, 109 insertions(+), 53 deletions(-) create mode 100644 plugins/app/src/apis/toastApiForwarderRef.ts diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index c209874e46..43c48cdc13 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -1938,7 +1938,6 @@ export const swappableComponentsApiRef: ApiRef_2; // @public export type ToastApi = { post(toast: ToastApiMessage): ToastApiPostResult; - toast$(): Observable; }; // @public @@ -1946,14 +1945,14 @@ export type ToastApiMessage = { title: ReactNode; description?: ReactNode; status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; - links?: ToastLink[]; + links?: ToastApiMessageLink[]; timeout?: number; }; // @public -export type ToastApiMessageWithKey = ToastApiMessage & { - key: string; - close(): void; +export type ToastApiMessageLink = { + label: string; + href: string; }; // @public @@ -1964,12 +1963,6 @@ export type ToastApiPostResult = { // @public export const toastApiRef: ApiRef; -// @public -export type ToastLink = { - label: string; - href: string; -}; - // @public (undocumented) export type TranslationApi = { getTranslation< diff --git a/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts b/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts index b56c9bd5da..0a76632c29 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/ToastApi.ts @@ -15,7 +15,6 @@ */ import { createApiRef, ApiRef } from '../system'; -import { Observable } from '@backstage/types'; import { ReactNode } from 'react'; /** @@ -23,7 +22,7 @@ import { ReactNode } from 'react'; * * @public */ -export type ToastLink = { +export type ToastApiMessageLink = { /** Display text for the link */ label: string; /** URL the link points to */ @@ -43,7 +42,7 @@ export type ToastApiMessage = { /** Status variant of the toast - defaults to 'success' */ status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; /** Optional array of links to display */ - links?: ToastLink[]; + links?: ToastApiMessageLink[]; /** Timeout in milliseconds before auto-dismiss. If not set, toast is permanent. */ timeout?: number; }; @@ -59,25 +58,6 @@ export type ToastApiPostResult = { close(): void; }; -/** - * Toast message with key, as emitted by the toast$() observable. - * - * @public - */ -export type ToastApiMessageWithKey = ToastApiMessage & { - /** Unique key for the toast, used internally for tracking */ - key: string; - /** Dismiss this toast programmatically */ - close(): void; - /** - * Register a callback that fires when this toast is closed. - * Used internally by the toast display to sync with the rendering queue. - * - * @internal - */ - onClose(callback: () => void): void; -}; - /** * The toast API is used to display toast notifications to the user. * @@ -117,11 +97,6 @@ export type ToastApi = { * @returns A handle with a `close()` method to programmatically dismiss the toast */ post(toast: ToastApiMessage): ToastApiPostResult; - - /** - * Observe toasts posted by other parts of the application. - */ - toast$(): Observable; }; /** diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 6389ea7c4c..afcaa002d5 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -714,6 +714,21 @@ const appPlugin: OverridableFrontendPlugin< params: ApiFactory, ) => ExtensionBlueprintParams; }>; + 'api:app/toast-forwarder': OverridableExtensionDefinition<{ + kind: 'api'; + name: 'toast-forwarder'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; 'api:app/translations': OverridableExtensionDefinition<{ config: {}; configInput: {}; diff --git a/plugins/app/src/apis/ToastApiForwarder.ts b/plugins/app/src/apis/ToastApiForwarder.ts index 5dc7b7e240..10a778f388 100644 --- a/plugins/app/src/apis/ToastApiForwarder.ts +++ b/plugins/app/src/apis/ToastApiForwarder.ts @@ -15,13 +15,15 @@ */ import { - ToastApi, ToastApiMessage, - ToastApiMessageWithKey, ToastApiPostResult, } from '@backstage/frontend-plugin-api'; import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; +import { + ToastApiForwarderApi, + ToastApiForwarderMessage, +} from './toastApiForwarderRef'; let toastKeyCounter = 0; @@ -85,9 +87,9 @@ class PublishSubject { * * @internal */ -export class ToastApiForwarder implements ToastApi { - private readonly subject = new PublishSubject(); - private readonly recentToasts: ToastApiMessageWithKey[] = []; +export class ToastApiForwarder implements ToastApiForwarderApi { + private readonly subject = new PublishSubject(); + private readonly recentToasts: ToastApiForwarderMessage[] = []; private readonly closedKeys = new Set(); private readonly maxBufferSize = 10; @@ -126,7 +128,7 @@ export class ToastApiForwarder implements ToastApi { } }; - const toastWithKey: ToastApiMessageWithKey = { + const toastWithKey: ToastApiForwarderMessage = { ...toast, key, close, @@ -142,7 +144,7 @@ export class ToastApiForwarder implements ToastApi { return { close }; } - toast$(): Observable { + toast$(): Observable { // Filter out any toasts that were closed to handle race conditions const activeToasts = this.recentToasts.filter( t => !this.closedKeys.has(t.key), diff --git a/plugins/app/src/apis/index.ts b/plugins/app/src/apis/index.ts index 7d32d25ef4..dc8189b7ee 100644 --- a/plugins/app/src/apis/index.ts +++ b/plugins/app/src/apis/index.ts @@ -15,3 +15,5 @@ */ export { ToastApiForwarder } from './ToastApiForwarder'; +export { toastApiForwarderRef } from './toastApiForwarderRef'; +export type { ToastApiForwarderApi } from './toastApiForwarderRef'; diff --git a/plugins/app/src/apis/toastApiForwarderRef.ts b/plugins/app/src/apis/toastApiForwarderRef.ts new file mode 100644 index 0000000000..2b2ddce0ab --- /dev/null +++ b/plugins/app/src/apis/toastApiForwarderRef.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2025 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 { + ToastApi, + ToastApiMessage, + createApiRef, +} from '@backstage/frontend-plugin-api'; +import { Observable } from '@backstage/types'; + +/** + * Internal enriched toast message emitted by the forwarder's observable. + * + * @internal + */ +export type ToastApiForwarderMessage = ToastApiMessage & { + /** Unique key for tracking this toast */ + key: string; + /** Dismiss this toast programmatically */ + close(): void; + /** + * Register a callback that fires when this toast is closed. + * Used by the toast display to sync with the rendering queue. + */ + onClose(callback: () => void): void; +}; + +/** + * Internal extension of the public ToastApi that adds an observable + * for the toast display to subscribe to. This interface is only used + * within the app plugin and can be freely evolved. + * + * @internal + */ +export type ToastApiForwarderApi = ToastApi & { + /** Observe toasts posted by other parts of the application. */ + toast$(): Observable; +}; + +/** + * Internal API ref for the toast forwarder. The public `toastApiRef` + * delegates to this, and the toast display subscribes to it directly. + * + * @internal + */ +export const toastApiForwarderRef = createApiRef({ + id: 'app.toast.internal-forwarder', +}); diff --git a/plugins/app/src/components/Toast/ToastDisplay.test.tsx b/plugins/app/src/components/Toast/ToastDisplay.test.tsx index ec565f1b66..b6d50136b0 100644 --- a/plugins/app/src/components/Toast/ToastDisplay.test.tsx +++ b/plugins/app/src/components/Toast/ToastDisplay.test.tsx @@ -23,11 +23,10 @@ import { appThemeApiRef, AppThemeApi, } from '@backstage/core-plugin-api'; -import { toastApiRef } from '@backstage/frontend-plugin-api'; import { Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; import { ToastDisplay } from './ToastDisplay'; -import { ToastApiForwarder } from '../../apis'; +import { ToastApiForwarder, toastApiForwarderRef } from '../../apis'; // Mock AlertApi with proper Observable implementation class MockAlertApi implements AlertApi { @@ -88,7 +87,7 @@ describe('ToastDisplay', () => { diff --git a/plugins/app/src/components/Toast/ToastDisplay.tsx b/plugins/app/src/components/Toast/ToastDisplay.tsx index 059f9e8231..e885342c1d 100644 --- a/plugins/app/src/components/Toast/ToastDisplay.tsx +++ b/plugins/app/src/components/Toast/ToastDisplay.tsx @@ -16,9 +16,9 @@ import { useEffect, useState } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; -import { toastApiRef } from '@backstage/frontend-plugin-api'; import { ToastQueue } from '@react-stately/toast'; import { ToastContainer } from './ToastContainer'; +import { toastApiForwarderRef } from '../../apis'; import type { ToastApiMessageDisplayProps, ToastApiMessageContent, @@ -85,7 +85,7 @@ function mapSeverity( */ export function ToastDisplay(props: ToastApiMessageDisplayProps) { const alertApi = useApi(alertApiRef); - const toastApi = useApi(toastApiRef); + const toastApi = useApi(toastApiForwarderRef); const { transientTimeoutMs = 5000 } = props; // Create toast queue once per component instance diff --git a/plugins/app/src/defaultApis.ts b/plugins/app/src/defaultApis.ts index b990947e74..febf7b258d 100644 --- a/plugins/app/src/defaultApis.ts +++ b/plugins/app/src/defaultApis.ts @@ -37,7 +37,7 @@ import { VMwareCloudAuth, OpenShiftAuth, } from '../../../packages/core-app-api/src/apis/implementations'; -import { ToastApiForwarder } from './apis'; +import { ToastApiForwarder, toastApiForwarderRef } from './apis'; import { alertApiRef, @@ -108,13 +108,22 @@ export const apis = [ factory: () => new AlertApiForwarder(), }), }), + ApiBlueprint.make({ + name: 'toast-forwarder', + params: defineParams => + defineParams({ + api: toastApiForwarderRef, + deps: {}, + factory: () => new ToastApiForwarder(), + }), + }), ApiBlueprint.make({ name: 'toast', params: defineParams => defineParams({ api: toastApiRef, - deps: {}, - factory: () => new ToastApiForwarder(), + deps: { forwarder: toastApiForwarderRef }, + factory: ({ forwarder }) => forwarder, }), }), analyticsApi,