Merge pull request #32542 from backstage/bui-toast

feat: Introduce ToastApi for rich notifications in the new frontend system
This commit is contained in:
Patrik Oldsberg
2026-03-17 15:34:16 +01:00
committed by GitHub
35 changed files with 2994 additions and 13 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ import { StorageValueSnapshot } from '@backstage/core-plugin-api';
import { SubRouteRef } from '@backstage/core-plugin-api';
import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api';
// @public
// @public @deprecated
export class AlertApiForwarder implements AlertApi {
// (undocumented)
alert$(): Observable<AlertMessage>;
@@ -26,13 +26,25 @@ import ObservableImpl from 'zen-observable';
* missing alerts that were posted before subscription.
*
* @public
* @deprecated Use ToastApi instead. AlertApi will be removed in a future release.
*/
export class AlertApiForwarder implements AlertApi {
private readonly subject = new PublishSubject<AlertMessage>();
private readonly recentAlerts: AlertMessage[] = [];
private readonly maxBufferSize = 10;
private hasWarnedDeprecation = false;
post(alert: AlertMessage) {
if (!this.hasWarnedDeprecation) {
this.hasWarnedDeprecation = true;
// eslint-disable-next-line no-console
console.warn(
'AlertApi is deprecated and will be removed in a future release. ' +
'Please migrate to ToastApi from @backstage/frontend-plugin-api. ' +
'ToastApi provides richer features including title/description, links, icons, and per-toast timeouts. ' +
'Example: toastApi.post({ title: "Saved!", status: "success", timeout: 5000 })',
);
}
this.recentAlerts.push(alert);
if (this.recentAlerts.length > this.maxBufferSize) {
this.recentAlerts.shift();
@@ -14,6 +14,23 @@
* limitations under the License.
*/
/**
* @deprecated AlertApi is deprecated. Use ToastApi from `@backstage/frontend-plugin-api` instead.
*
* ToastApi provides richer notification features including title/description,
* action links, custom icons, per-toast timeout control, and programmatic dismiss.
*
* @example
* ```typescript
* // Before (AlertApi)
* import { alertApiRef } from '@backstage/core-plugin-api';
* alertApi.post({ message: 'Saved!', severity: 'success', display: 'transient' });
*
* // After (ToastApi)
* import { toastApiRef } from '@backstage/frontend-plugin-api';
* toastApi.post({ title: 'Saved!', status: 'success', timeout: 5000 });
* ```
*/
export {
type AlertApi,
type AlertMessage,
@@ -1026,6 +1026,8 @@ describe('createApp', () => {
<api:app/dialog out=[core.api.factory] />
<api:app/discovery out=[core.api.factory] />
<api:app/alert out=[core.api.factory] />
<api:app/toast-forwarder out=[core.api.factory] />
<api:app/toast out=[core.api.factory] />
<api:app/analytics out=[core.api.factory] />
<api:app/error out=[core.api.factory] />
<api:app/storage out=[core.api.factory] />
+31 -3
View File
@@ -25,18 +25,18 @@ import { ReactNode } from 'react';
import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api';
import type { z } from 'zod';
// @public
// @public @deprecated
export type AlertApi = {
post(alert: AlertMessage): void;
alert$(): Observable<AlertMessage>;
};
// @public
// @public @deprecated
export const alertApiRef: ApiRef_2<AlertApi, 'core.alert'> & {
readonly $$type: '@backstage/ApiRef';
};
// @public
// @public @deprecated
export type AlertMessage = {
message: string;
severity?: 'success' | 'info' | 'warning' | 'error';
@@ -2241,6 +2241,34 @@ export const swappableComponentsApiRef: ApiRef_2<
readonly $$type: '@backstage/ApiRef';
};
// @public
export type ToastApi = {
post(toast: ToastApiMessage): ToastApiPostResult;
};
// @public
export type ToastApiMessage = {
title: ReactNode;
description?: ReactNode;
status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
links?: ToastApiMessageLink[];
timeout?: number;
};
// @public
export type ToastApiMessageLink = {
label: string;
href: string;
};
// @public
export type ToastApiPostResult = {
close(): void;
};
// @public
export const toastApiRef: ApiRef<ToastApi>;
// @public (undocumented)
export type TranslationApi = {
getTranslation<
@@ -21,6 +21,14 @@ import { Observable } from '@backstage/types';
* Message handled by the {@link AlertApi}.
*
* @public
* @deprecated Use {@link ToastApiMessage} from {@link ToastApi} instead. AlertApi will be removed in a future release.
*
* Migration guide:
* - `message` becomes `title`
* - `severity: 'error'` becomes `status: 'danger'`
* - `severity: 'success' | 'info' | 'warning'` becomes `status: 'success' | 'info' | 'warning'`
* - `display: 'transient'` becomes `timeout: 5000` (or custom milliseconds)
* - `display: 'permanent'` means omitting `timeout`
*/
export type AlertMessage = {
message: string;
@@ -33,6 +41,23 @@ export type AlertMessage = {
* The alert API is used to report alerts to the app, and display them to the user.
*
* @public
* @deprecated Use {@link ToastApi} instead. AlertApi will be removed in a future release.
*
* ToastApi provides richer notification features including:
* - Title and optional description
* - Action links
* - Custom icons
* - Per-toast timeout control
* - Programmatic dismiss via returned key
*
* @example
* ```typescript
* // Before (AlertApi)
* alertApi.post({ message: 'Saved!', severity: 'success', display: 'transient' });
*
* // After (ToastApi)
* toastApi.post({ title: 'Saved!', status: 'success', timeout: 5000 });
* ```
*/
export type AlertApi = {
/**
@@ -50,6 +75,7 @@ export type AlertApi = {
* The {@link ApiRef} of {@link AlertApi}.
*
* @public
* @deprecated Use {@link toastApiRef} instead. AlertApi will be removed in a future release.
*/
export const alertApiRef = createApiRef<AlertApi>().with({
id: 'core.alert',
@@ -0,0 +1,109 @@
/*
* 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 { createApiRef, ApiRef } from '../system';
import { ReactNode } from 'react';
/**
* Link item for toast notifications.
*
* @public
*/
export type ToastApiMessageLink = {
/** Display text for the link */
label: string;
/** URL the link points to */
href: string;
};
/**
* Message handled by the {@link ToastApi}.
*
* @public
*/
export type ToastApiMessage = {
/** Title of the toast (required) */
title: ReactNode;
/** Optional description text */
description?: ReactNode;
/** Status variant of the toast - defaults to 'success' */
status?: 'neutral' | 'info' | 'success' | 'warning' | 'danger';
/** Optional array of links to display */
links?: ToastApiMessageLink[];
/** Timeout in milliseconds before auto-dismiss. If not set, toast is permanent. */
timeout?: number;
};
/**
* Handle returned by {@link ToastApi.post} that allows programmatic control
* of the posted toast.
*
* @public
*/
export type ToastApiPostResult = {
/** Dismiss the toast. */
close(): void;
};
/**
* The toast API is used to display toast notifications to the user.
*
* @remarks
* This API provides richer notification capabilities than the AlertApi,
* including title/description, links, and per-toast timeout control.
*
* @example
* ```tsx
* const toastApi = useApi(toastApiRef);
*
* // Full-featured toast
* toastApi.post({
* title: 'Entity saved',
* description: 'Your changes have been saved successfully.',
* status: 'success',
* timeout: 5000,
* links: [{ label: 'View entity', href: '/catalog/default/component/my-service' }],
* });
*
* // Simple toast
* toastApi.post({ title: 'Processing...', status: 'info' });
*
* // Programmatic dismiss
* const { close } = toastApi.post({ title: 'Uploading...', status: 'info' });
* // Later...
* close();
* ```
*
* @public
*/
export type ToastApi = {
/**
* Post a toast notification for display to the user.
*
* @param toast - The toast message to display
* @returns A handle with a `close()` method to programmatically dismiss the toast
*/
post(toast: ToastApiMessage): ToastApiPostResult;
};
/**
* The {@link ApiRef} of {@link ToastApi}.
*
* @public
*/
export const toastApiRef: ApiRef<ToastApi> = createApiRef({
id: 'core.toast',
});
@@ -48,6 +48,7 @@ export * from './OAuthRequestApi';
export * from './RouteResolutionApi';
export * from './StorageApi';
export * from './AnalyticsApi';
export * from './ToastApi';
export * from './TranslationApi';
export * from './PluginHeaderActionsApi';
export * from './PluginWrapperApi';
+4 -3
View File
@@ -15,8 +15,9 @@
*/
@layer tokens {
/* Light theme tokens */
:root {
/* Light theme tokens (also used for nested light theme switching) */
:root,
[data-theme-mode='light'] {
/* Font families */
--bui-font-regular: system-ui;
--bui-font-monospace: ui-monospace, 'Menlo', 'Monaco', 'Consolas',
@@ -119,7 +120,7 @@
--bui-fg-info-on-bg: #173da6;
--bui-fg-danger: #ec3b18;
--bui-fg-warning: #ef7a32;
--bui-fg-success: #1ed760;
--bui-fg-success: #1aaf4f;
--bui-fg-info: #0d74ce;
/* Border colors */