frontend-plugin-api,app: move toast forwading to be internal

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-17 14:11:04 +01:00
parent 564fe9f948
commit da5a35a38c
9 changed files with 109 additions and 53 deletions
+4 -11
View File
@@ -1938,7 +1938,6 @@ export const swappableComponentsApiRef: ApiRef_2<SwappableComponentsApi>;
// @public
export type ToastApi = {
post(toast: ToastApiMessage): ToastApiPostResult;
toast$(): Observable<ToastApiMessageWithKey>;
};
// @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<ToastApi>;
// @public
export type ToastLink = {
label: string;
href: string;
};
// @public (undocumented)
export type TranslationApi = {
getTranslation<
@@ -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<ToastApiMessageWithKey>;
};
/**
+15
View File
@@ -714,6 +714,21 @@ const appPlugin: OverridableFrontendPlugin<
params: ApiFactory<TApi, TImpl, TDeps>,
) => ExtensionBlueprintParams<AnyApiFactory>;
}>;
'api:app/toast-forwarder': OverridableExtensionDefinition<{
kind: 'api';
name: 'toast-forwarder';
config: {};
configInput: {};
output: ExtensionDataRef<AnyApiFactory, 'core.api.factory', {}>;
inputs: {};
params: <
TApi,
TImpl extends TApi,
TDeps extends { [name in string]: unknown },
>(
params: ApiFactory<TApi, TImpl, TDeps>,
) => ExtensionBlueprintParams<AnyApiFactory>;
}>;
'api:app/translations': OverridableExtensionDefinition<{
config: {};
configInput: {};
+9 -7
View File
@@ -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<T> {
*
* @internal
*/
export class ToastApiForwarder implements ToastApi {
private readonly subject = new PublishSubject<ToastApiMessageWithKey>();
private readonly recentToasts: ToastApiMessageWithKey[] = [];
export class ToastApiForwarder implements ToastApiForwarderApi {
private readonly subject = new PublishSubject<ToastApiForwarderMessage>();
private readonly recentToasts: ToastApiForwarderMessage[] = [];
private readonly closedKeys = new Set<string>();
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<ToastApiMessageWithKey> {
toast$(): Observable<ToastApiForwarderMessage> {
// Filter out any toasts that were closed to handle race conditions
const activeToasts = this.recentToasts.filter(
t => !this.closedKeys.has(t.key),
+2
View File
@@ -15,3 +15,5 @@
*/
export { ToastApiForwarder } from './ToastApiForwarder';
export { toastApiForwarderRef } from './toastApiForwarderRef';
export type { ToastApiForwarderApi } from './toastApiForwarderRef';
@@ -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<ToastApiForwarderMessage>;
};
/**
* 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<ToastApiForwarderApi>({
id: 'app.toast.internal-forwarder',
});
@@ -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', () => {
<TestApiProvider
apis={[
[alertApiRef, alertApi],
[toastApiRef, toastApi],
[toastApiForwarderRef, toastApi],
[appThemeApiRef, mockAppThemeApi],
]}
>
@@ -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
+12 -3
View File
@@ -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,