From 90956a61bdc85eca613184e78e66a5d12dfc5f2d Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Mon, 22 Sep 2025 10:00:11 -0700 Subject: [PATCH 01/12] feat(home): add new frontend system support Migrates home plugin to support the new frontend system architecture by introducing extension blueprints for composable homepage functionality. Key changes: - Add CustomHomepageWidgetBlueprint for creating installable homepage widgets - Add CustomHomepageBlueprint for composing pages from widget extensions - Introduce titleExtensionDataRef for NFS title handling This attempts to bring the home plugin up to par with other core plugins that have migrated to the new frontend system Signed-off-by: Adam Kunicki --- .changeset/tangy-wasps-invent.md | 6 + docs/getting-started/homepage.md | 78 ++++++- packages/app-next/package.json | 1 + packages/app-next/src/App.tsx | 5 +- plugins/home-react/package.json | 1 + plugins/home-react/report-alpha.api.md | 88 +++++++- plugins/home-react/src/alpha.ts | 17 ++ .../blueprints/HomepageWidgetBlueprint.tsx | 128 +++++++++++ plugins/home-react/src/alpha/dataRefs.ts | 26 +++ plugins/home-react/src/extensions.tsx | 2 +- plugins/home-react/src/translation.ts | 3 + plugins/home/README.md | 165 ++++++++++++-- plugins/home/dev/index.tsx | 213 +++++++++++++++++- plugins/home/package.json | 2 + plugins/home/report-alpha.api.md | 107 ++++++++- plugins/home/report.api.md | 2 +- plugins/home/src/alpha.test.ts | 111 +++++++++ plugins/home/src/alpha.tsx | 61 ++++- plugins/home/src/alpha/HomepageBlueprint.tsx | 115 ++++++++++ plugins/home/src/api/VisitsApi.ts | 7 +- plugins/home/src/translation.ts | 3 + yarn.lock | 4 + 22 files changed, 1085 insertions(+), 60 deletions(-) create mode 100644 .changeset/tangy-wasps-invent.md create mode 100644 plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx create mode 100644 plugins/home-react/src/alpha/dataRefs.ts create mode 100644 plugins/home/src/alpha.test.ts create mode 100644 plugins/home/src/alpha/HomepageBlueprint.tsx diff --git a/.changeset/tangy-wasps-invent.md b/.changeset/tangy-wasps-invent.md new file mode 100644 index 0000000000..6b07726df0 --- /dev/null +++ b/.changeset/tangy-wasps-invent.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-home-react': patch +'@backstage/plugin-home': patch +--- + +Support new frontend system in the homepage plugin diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 7fac59f728..8faedd67eb 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -24,7 +24,83 @@ Before we begin, make sure Now, let's get started by installing the home plugin and creating a simple homepage for your Backstage app. -### Setup homepage +## Setup Methods + +There are two ways to set up the home plugin, depending on which frontend system your Backstage app uses: + +1. **New Frontend System (Recommended)** - For apps using the new plugin system with extensions and blueprints +2. **Legacy Frontend System** - For existing apps using the legacy plugin architecture + +### New Frontend System Setup + +If your Backstage app uses the [new frontend system](../frontend-system/index.md), follow these steps: + +#### 1. Install the plugin + +```bash title="From your Backstage root directory" +yarn --cwd packages/app add @backstage/plugin-home +``` + +#### 2. Add the plugin to your app configuration + +Update your `packages/app/src/app.tsx` to include the home plugin: + +```tsx title="packages/app/src/app.tsx" +import homePlugin from '@backstage/plugin-home/alpha'; + +const app = createApp({ + features: [ + // ... other plugins + homePlugin, + ], +}); +``` + +#### 3. Configure the homepage as your root route + +By default, the homepage will be available at `/home`. To make it your app's landing page at `/`, add this configuration to your `app-config.yaml`: + +```yaml title="app-config.yaml" +app: + extensions: + - page:home: + config: + path: / +``` + +The plugin will automatically add a "Home" navigation item to your sidebar and provide a basic homepage layout. + +#### 4. Optional: Enable visit tracking + +Visit tracking is an optional feature that allows users to see their recently visited and most visited pages on the homepage. This feature is **disabled by default** to give you control over what data is collected and stored. + +Visit tracking requires a storage implementation to persist user data: + +- **With UserSettings storage** (recommended): If you have the [UserSettings plugin](https://backstage.io/docs/features/software-catalog/external-integrations/#user-settings) configured with persistent storage, visit data will be stored there and synchronized across devices. +- **Fallback to local storage**: If no persistent storage is available, the plugin will automatically fall back to browser local storage, which stores data locally per device. + +To enable visit tracking, add this configuration to your `app-config.yaml`: + +```yaml title="app-config.yaml" +app: + extensions: + - api:home/visits: true + - app-root-element:home/visit-listener: true +``` + +#### 5. Customizing your homepage + +The New Frontend System provides powerful customization options: + +**Custom Homepage Layouts**: Use the `HomepageBlueprint` to create custom homepage layouts with your own design and widget arrangements. + +**Adding Homepage Widgets**: Register custom widgets using the `HomepageWidgetBlueprint` from the `@backstage/plugin-home-react/alpha` package. + +For detailed instructions on creating custom layouts, registering widgets, and advanced configuration options, see the [Home plugin documentation](https://github.com/backstage/backstage/tree/master/plugins/home#readme). + +### Legacy Frontend System Setup + +If your Backstage app uses the legacy frontend system, follow these steps: #### 1. Install the plugin diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 411142e8d4..b974969f6c 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -60,6 +60,7 @@ "@backstage/plugin-catalog-unprocessed-entities": "workspace:^", "@backstage/plugin-devtools": "workspace:^", "@backstage/plugin-home": "workspace:^", + "@backstage/plugin-home-react": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-kubernetes-cluster": "workspace:^", "@backstage/plugin-notifications": "workspace:^", diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 3503805394..df63ccd5c5 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -18,9 +18,8 @@ import { createApp } from '@backstage/frontend-defaults'; import { pagesPlugin } from './examples/pagesPlugin'; import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; -import homePlugin, { - titleExtensionDataRef, -} from '@backstage/plugin-home/alpha'; +import homePlugin from '@backstage/plugin-home/alpha'; +import { titleExtensionDataRef } from '@backstage/plugin-home-react/alpha'; import { coreExtensionData, diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index 298b690841..a5df160e73 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -55,6 +55,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index fad4c5a2bc..1689a91f73 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -3,9 +3,77 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { RJSFSchema } from '@rjsf/utils'; import { TranslationRef } from '@backstage/frontend-plugin-api'; +import { UiSchema } from '@rjsf/utils'; + +// @public (undocumented) +export type CardLayout = { + width?: { + minColumns?: number; + maxColumns?: number; + defaultColumns?: number; + }; + height?: { + minRows?: number; + maxRows?: number; + defaultRows?: number; + }; +}; + +// @public (undocumented) +export type CardSettings = { + schema?: RJSFSchema; + uiSchema?: UiSchema; +}; + +// @public (undocumented) +export type ComponentParts = { + Content: (props?: any) => JSX.Element; + Actions?: () => JSX.Element; + Settings?: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}; + +// @alpha +export const HomepageWidgetBlueprint: ExtensionBlueprint<{ + kind: 'home-widget'; + params: HomepageWidgetBlueprintParams; + output: + | ExtensionDataRef + | ExtensionDataRef< + { + name?: string; + title?: string; + description?: string; + layout?: CardLayout; + settings?: CardSettings; + }, + 'home.widget.metadata', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: never; +}>; // @alpha (undocumented) +export interface HomepageWidgetBlueprintParams { + componentProps?: Record; + components: () => Promise; + description?: string; + layout?: CardLayout; + name?: string; + settings?: CardSettings; + title?: string; +} + +// @alpha export const homeReactTranslationRef: TranslationRef< 'home-react', { @@ -15,5 +83,23 @@ export const homeReactTranslationRef: TranslationRef< } >; -// (No @packageDocumentation comment for this package) +// @alpha +export const titleExtensionDataRef: ConfigurableExtensionDataRef< + string, + 'title', + {} +>; + +// @alpha +export const widgetMetadataRef: ConfigurableExtensionDataRef< + { + name?: string; + title?: string; + description?: string; + layout?: CardLayout; + settings?: CardSettings; + }, + 'home.widget.metadata', + {} +>; ``` diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 3a08699f66..b72ba1972b 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -13,4 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/** + * React components and utilities for the home plugin's new frontend system. + * + * @remarks + * This package provides React components, blueprints, and utilities for building + * customizable home pages with the new Backstage frontend system. + * + * @packageDocumentation + */ export { homeReactTranslationRef } from './translation'; +export { titleExtensionDataRef } from './alpha/dataRefs'; +export { + HomepageWidgetBlueprint, + widgetMetadataRef, + type HomepageWidgetBlueprintParams, +} from './alpha/blueprints/HomepageWidgetBlueprint'; +export type { ComponentParts, CardLayout, CardSettings } from './extensions'; diff --git a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx new file mode 100644 index 0000000000..553a6b3249 --- /dev/null +++ b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx @@ -0,0 +1,128 @@ +/* + * 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 { lazy, ReactElement } from 'react'; +import { compatWrapper } from '@backstage/core-compat-api'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionDataRef, + ExtensionBoundary, +} from '@backstage/frontend-plugin-api'; +import { + CardExtension, + CardExtensionProps, + CardLayout, + CardSettings, + ComponentParts, +} from '../../extensions'; + +/** @alpha */ +export interface HomepageWidgetBlueprintParams { + /** + * Optional name for the widget. If not provided, the extension will use only its kind + * in the extension ID. + */ + name?: string; + /** + * Optional title displayed for the widget, used as the default card heading. + */ + title?: string; + /** + * Optional description shown in the widget catalog when adding new cards. + */ + description?: string; + /** + * Component parts rendered within the card. + */ + components: () => Promise; + /** + * Layout hints used by the customizable grid. + */ + layout?: CardLayout; + /** + * Schema used to configure widget settings. + */ + settings?: CardSettings; + /** + * Default props forwarded to the rendered widget component. + */ + componentProps?: Record; +} + +const DEFAULT_WIDGET_ATTACH_POINT = { + id: 'page:home', + input: 'widgets', +} as const; + +/** + * Extension data ref for widget metadata. + * @alpha + */ +export const widgetMetadataRef = createExtensionDataRef<{ + name?: string; + title?: string; + description?: string; + layout?: CardLayout; + settings?: CardSettings; +}>().with({ id: 'home.widget.metadata' }); + +/** + * Creates widgets that can be installed into the home page grid. + * + * @alpha + */ +export const HomepageWidgetBlueprint = createExtensionBlueprint({ + kind: 'home-widget', + attachTo: DEFAULT_WIDGET_ATTACH_POINT, + output: [coreExtensionData.reactElement, widgetMetadataRef], + *factory(params: HomepageWidgetBlueprintParams, { node }) { + const isCustomizable = params.settings?.schema !== undefined; + const LazyCard = lazy(() => + params.components().then(parts => ({ + default: (props: CardExtensionProps>) => ( + + ), + })), + ); + + const Widget = ( + props: CardExtensionProps>, + ): ReactElement => + compatWrapper( + + + , + ); + + yield coreExtensionData.reactElement( + , + ); + + yield widgetMetadataRef({ + name: params.name, + title: params.title, + description: params.description, + layout: params.layout, + settings: params.settings, + }); + }, +}); diff --git a/plugins/home-react/src/alpha/dataRefs.ts b/plugins/home-react/src/alpha/dataRefs.ts new file mode 100644 index 0000000000..5c15f9b527 --- /dev/null +++ b/plugins/home-react/src/alpha/dataRefs.ts @@ -0,0 +1,26 @@ +/* + * 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 { createExtensionDataRef } from '@backstage/frontend-plugin-api'; + +/** + * Title data supplied to the home page extension when composing via the new frontend system. + * + * @alpha + */ +export const titleExtensionDataRef = createExtensionDataRef().with({ + id: 'title', +}); diff --git a/plugins/home-react/src/extensions.tsx b/plugins/home-react/src/extensions.tsx index 7795e20c73..699a3f9c9f 100644 --- a/plugins/home-react/src/extensions.tsx +++ b/plugins/home-react/src/extensions.tsx @@ -121,7 +121,7 @@ type CardExtensionComponentProps = CardExtensionProps & overrideTitle?: string; }; -function CardExtension(props: CardExtensionComponentProps) { +export function CardExtension(props: CardExtensionComponentProps) { const { Renderer, Content, diff --git a/plugins/home-react/src/translation.ts b/plugins/home-react/src/translation.ts index 8de5bb1ff1..1a8fd9f1bf 100644 --- a/plugins/home-react/src/translation.ts +++ b/plugins/home-react/src/translation.ts @@ -16,6 +16,9 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** + * Translation reference for the home-react plugin. + * Contains localized text strings for home page components and settings modals. + * * @alpha */ export const homeReactTranslationRef = createTranslationRef({ diff --git a/plugins/home/README.md b/plugins/home/README.md index 242e66ff40..10bebe6195 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -4,7 +4,7 @@ The Home plugin introduces a system for composing a Home Page for Backstage in o For App Integrators, the system is designed to be composable to give total freedom in designing a Home Page that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Home Pages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. -## Getting started +## Installation If you have a standalone app (you didn't clone this repo), then do @@ -13,6 +13,144 @@ If you have a standalone app (you didn't clone this repo), then do yarn --cwd packages/app add @backstage/plugin-home ``` +## Getting started + +The home plugin supports both the new frontend system and the legacy system. + +### New Frontend System + +If you're using Backstage's new frontend system, add the plugin to your app: + +```ts +// packages/app/src/App.tsx +import homePlugin from '@backstage/plugin-home/alpha'; + +const app = createApp({ + features: [ + // ... other plugins + homePlugin, + // ... other plugins + ], +}); +``` + +The plugin will automatically provide: + +- A homepage at `/home` with customizable widget grid +- A "Home" navigation item in the sidebar + +#### Creating Custom Homepage Layouts + +Use the `HomepageBlueprint` to create custom homepage layouts: + +```ts +import { HomepageBlueprint } from '@backstage/plugin-home/alpha'; +import { Content, Header, Page } from '@backstage/core-components'; + +const myHomePage = HomepageBlueprint.make({ + params: { + title: 'My Custom Home', + render: ({ grid }) => ( + +
+ {grid} + + ), + }, +}); +``` + +#### Visit Tracking (Optional) + +Visit tracking is an **optional feature** that must be explicitly enabled. When enabled, it provides intelligent storage fallbacks: + +**Enabling Visit Tracking:** + +Add the following to your `app-config.yaml`: + +```yaml +app: + extensions: + # Enable visit tracking API (disabled by default) + - api:home/visits: true + # Enable visit listener (disabled by default) + - app-root-element:home/visit-listener: true +``` + +**Storage Strategy (when enabled):** + +1. **Custom Storage API**: If you have `storageApiRef` configured (like database-backed `UserSettingsStorage`), visit data uses your custom storage +2. **Browser Local Storage Fallback**: If no custom storage is configured, automatically falls back to browser local storage + +**Note**: Visit tracking extensions are disabled by default to give users control over data collection and storage. + +## Creating Homepage Widgets + +Homepage widgets are React components that can be added to customizable home pages. The **key difference** between the new frontend system and legacy system is how these widget components are **registered and exported**: + +- **New Frontend System**: Use `HomepageWidgetBlueprint` to register widgets as extensions +- **Legacy System**: Use `createCardExtension` to export widgets as card components + +### New Frontend System + +Create widgets using the `HomepageWidgetBlueprint`: + +```ts +import { HomepageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; + +const myWidget = HomepageWidgetBlueprint.make({ + name: 'my-widget', + params: { + name: 'MyWidget', + title: 'My Custom Widget', + description: 'A custom widget for the homepage', + components: () => + import('./MyWidgetComponent').then(m => ({ + Content: m.Content, + })), + layout: { + height: { minRows: 4 }, + width: { minColumns: 3 }, + }, + settings: { + schema: { + title: 'Widget Settings', + type: 'object', + properties: { + color: { + title: 'Color', + type: 'string', + default: 'blue', + enum: ['blue', 'red', 'green'], + }, + }, + }, + }, + }, +}); +``` + +> **Example**: See [dev/index.tsx](dev/index.tsx) for a comprehensive example of creating multiple homepage widgets and layouts using the new frontend system. + +### Legacy System - Widget Registration + +In the legacy system, use the `createCardExtension` helper to create homepage widgets: + +```tsx +import { createCardExtension } from '@backstage/plugin-home-react'; + +export const MyWidget = homePlugin.provide( + createCardExtension<{ defaultCategory?: 'programming' | 'any' }>({ + title: 'My Custom Widget', + components: () => import('./homePageComponents/MyWidget'), + }), +); +``` + +The `createCardExtension` provides error boundary and lazy loading, and accepts generics for custom props that App Integrators can configure. + +## Legacy System Setup + ### Setting up the Home Page 1. Create a Home Page Component that will be used for composition. @@ -40,28 +178,13 @@ import { homePage } from './components/home/HomePage'; // ... ``` -### Creating Components +### Creating Components (Legacy) -The Home Page can be composed with regular React components, so there's no magic in creating components to be used for composition 🪄 🎩 . However, in order to assure that your component fits into a diverse set of Home Pages, there's an extension creator for this purpose, that creates a Card-based layout, for consistency between components (read more about extensions [here](https://backstage.io/docs/plugins/composability#extensions)). The extension creator requires two fields: `title` and `components`. The `components` field is expected to be an asynchronous import that should at least contain a `Content` field. Additionally, you can optionally provide `settings`, `actions` and `contextProvider` as well. These parts will be combined to create a card, where the `content`, `actions` and `settings` will be wrapped within the `contextProvider` in order to be able to access to context and effectively communicate with one another. +In the legacy system, homepage components can be regular React components or wrapped with `createCardExtension` for additional features like error boundaries and lazy loading. Components created with `createCardExtension` are exported as card components that can be composed into homepage layouts. -Finally, the `createCardExtension` also accepts a generic, such that Component Developers can indicate to App Integrators what custom props their component will accept, such as the example below where the default category of the random jokes can be set. +### Composing a Home Page (Legacy) -```tsx -import { createCardExtension } from '@backstage/plugin-home-react'; - -export const RandomJokeHomePageComponent = homePlugin.provide( - createCardExtension<{ defaultCategory?: 'programming' | 'any' }>({ - title: 'Random Joke', - components: () => import('./homePageComponents/RandomJoke'), - }), -); -``` - -In summary: it is not necessary to use the `createCardExtension` extension creator to register a home page component, although it is convenient since it provides error boundary and lazy loading, and it also may hook into other functionality in the future. - -### Composing a Home Page - -Composing a Home Page is no different from creating a regular React Component, i.e. the App Integrator is free to include whatever content they like. However, there are components developed with the Home Page in mind, as described in the previous section. If created by the `createCardExtension` extension creator, they are rendered like so +In the legacy system, composing a Home Page is done by creating regular React components. Components created with `createCardExtension` are rendered like so: ```tsx import Grid from '@material-ui/core/Grid'; @@ -293,7 +416,7 @@ export const apis: AnyApiFactory[] = [ VisitsStorageApi.create({ storageApi, identityApi }), }), - // Or a localStorage data implementation, relies on WebStorage implementation of storageApi + // Or a local storage data implementation, relies on WebStorage implementation of storageApi createApiFactory({ api: visitsApiRef, deps: { diff --git a/plugins/home/dev/index.tsx b/plugins/home/dev/index.tsx index 341253a49e..edbcdc574b 100644 --- a/plugins/home/dev/index.tsx +++ b/plugins/home/dev/index.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Backstage Authors + * 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. @@ -13,14 +13,205 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createDevApp } from '@backstage/dev-utils'; -import { homePlugin, HomepageCompositionRoot } from '../src/plugin'; -createDevApp() - .registerPlugin(homePlugin) - .addPage({ - element: , - title: 'Root Page', - path: '/', - }) - .render(); +import { Content, Header, Page } from '@backstage/core-components'; +import { createApp } from '@backstage/frontend-defaults'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import HomeIcon from '@material-ui/icons/Home'; +import ReactDOM from 'react-dom/client'; + +import { + ApiBlueprint, + createFrontendModule, +} from '@backstage/frontend-plugin-api'; +import { HomepageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; +import { HeaderWorldClock, WelcomeTitle, type ClockConfig } from '../src'; +import homePlugin, { + HomepageBlueprint, + type HomepageGridProps, +} from '../src/alpha'; + +const clockConfigs: ClockConfig[] = [ + { label: 'NYC', timeZone: 'America/New_York' }, + { label: 'UTC', timeZone: 'UTC' }, + { label: 'STO', timeZone: 'Europe/Stockholm' }, + { label: 'TYO', timeZone: 'Asia/Tokyo' }, +]; + +const timeFormat: Intl.DateTimeFormatOptions = { + hour: '2-digit', + minute: '2-digit', + hour12: false, +}; + +const defaultGridConfig: NonNullable = [ + { + component: 'HomePageToolkit', + x: 0, + y: 0, + width: 12, + height: 4, + movable: false, + resizable: false, + }, + { + component: 'HomePageStarredEntities', + x: 0, + y: 4, + width: 6, + height: 5, + }, + { + component: 'HomePageRandomJoke', + x: 6, + y: 4, + width: 6, + height: 5, + }, +]; + +const homePage = HomepageBlueprint.make({ + params: { + title: 'Home', + grid: { + config: defaultGridConfig, + }, + render: ({ grid }) => ( + +
} pageTitleOverride="Home"> + +
+ {grid} +
+ ), + }, +}); + +const homePageToolkitWidget = HomepageWidgetBlueprint.make({ + name: 'home-toolkit', + params: { + name: 'HomePageToolkit', + title: 'Toolkit', + components: () => + import('../src/homePageComponents/Toolkit').then(m => ({ + Content: m.Content, + ContextProvider: m.ContextProvider, + })), + componentProps: { + tools: [ + { + url: 'https://backstage.io', + label: 'Backstage Homepage', + icon: , + }, + ], + }, + }, +}); + +const homePageStarredEntitiesWidget = HomepageWidgetBlueprint.make({ + name: 'home-starred-entities', + params: { + name: 'HomePageStarredEntities', + title: 'Your Starred Entities', + components: () => + import('../src/homePageComponents/StarredEntities').then(m => ({ + Content: m.Content, + })), + }, +}); + +const homePageRandomJokeWidget = HomepageWidgetBlueprint.make({ + name: 'home-random-joke', + params: { + name: 'HomePageRandomJoke', + title: 'Random Joke', + description: 'Shows a random programming joke', + components: () => + import('../src/homePageComponents/RandomJoke').then(m => ({ + Content: m.Content, + Settings: m.Settings, + Actions: m.Actions, + ContextProvider: m.ContextProvider, + })), + layout: { + height: { minRows: 4 }, + width: { minColumns: 3 }, + }, + settings: { + schema: { + title: 'Random Joke settings', + type: 'object', + properties: { + defaultCategory: { + title: 'Category', + type: 'string', + enum: ['any', 'programming', 'dad'], + default: 'any', + }, + }, + }, + }, + }, +}); + +const homeDevModule = createFrontendModule({ + pluginId: 'home', + extensions: [ + homePage, + homePageToolkitWidget, + homePageStarredEntitiesWidget, + homePageRandomJokeWidget, + ], +}); + +const entities = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'example', + annotations: { + 'backstage.io/managed-by-location': 'file:/path/to/catalog-info.yaml', + }, + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'guest', + }, + }, +]; + +const catalogApi = catalogApiMock({ entities }); + +const catalogPluginOverrides = createFrontendModule({ + pluginId: 'catalog', + extensions: [ + ApiBlueprint.make({ + params: defineParams => + defineParams({ + api: catalogApiRef, + deps: {}, + factory: () => catalogApi, + }), + }), + ], +}); + +const app = createApp({ + features: [ + catalogPlugin, + catalogPluginOverrides, + homePlugin, // Load the home plugin + homeDevModule, // Load the widgets and homepage content + ], +}); + +const root = app.createRoot(); +ReactDOM.createRoot(document.getElementById('root')!).render(root); diff --git a/plugins/home/package.json b/plugins/home/package.json index 8121f340c0..f8e2162fbd 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -83,6 +83,8 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/frontend-defaults": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^10.0.0", "@testing-library/jest-dom": "^6.0.0", diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 99e9b07463..7de07f408c 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -7,16 +7,24 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { CSSProperties } from 'react'; +import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { ReactElement } from 'react'; +import { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @public +export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +// @alpha const _default: OverridableFrontendPlugin< { root: RouteRef; @@ -49,6 +57,27 @@ const _default: OverridableFrontendPlugin< element: JSX.Element; }; }>; + 'nav-item:home': OverridableExtensionDefinition<{ + kind: 'nav-item'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef< + { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + inputs: {}; + params: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }; + }>; 'page:home': OverridableExtensionDefinition<{ config: { path: string | undefined; @@ -102,7 +131,64 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @alpha +export const HomepageBlueprint: ExtensionBlueprint<{ + kind: 'home-page'; + params: HomepageBlueprintParams; + output: + | ExtensionDataRef + | ExtensionDataRef< + string, + 'title', + { + optional: true; + } + >; + inputs: { + widgets: ExtensionInput< + ConfigurableExtensionDataRef, + { + singleton: false; + optional: false; + } + >; + }; + config: {}; + configInput: {}; + dataRefs: never; +}>; + +// @alpha +export interface HomepageBlueprintParams { + grid?: Omit; + render?: (props: HomepageTemplateProps) => ReactElement; + title?: string; +} + +// @public +export type HomepageGridProps = { + children?: ReactNode; + config?: LayoutConfiguration[]; + title?: string; + rowHeight?: number; + breakpoints?: Record; + cols?: Record; + containerPadding?: [number, number] | Record; + containerMargin?: [number, number] | Record; + maxRows?: number; + style?: CSSProperties; + compactType?: 'vertical' | 'horizontal' | null; + allowOverlap?: boolean; + preventCollision?: boolean; +}; + +// @alpha +export interface HomepageTemplateProps { + grid: ReactElement; + widgets: ReactNode[]; +} + +// @alpha export const homeTranslationRef: TranslationRef< 'home', { @@ -135,12 +221,17 @@ export const homeTranslationRef: TranslationRef< } >; -// @alpha (undocumented) -export const titleExtensionDataRef: ConfigurableExtensionDataRef< - string, - 'title', - {} ->; +// @public +export type LayoutConfiguration = { + component: ReactElement | string; + x: number; + y: number; + width: number; + height: number; + movable?: boolean; + deletable?: boolean; + resizable?: boolean; +}; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home/report.api.md b/plugins/home/report.api.md index 7b15ecd8a7..17870ed481 100644 --- a/plugins/home/report.api.md +++ b/plugins/home/report.api.md @@ -344,7 +344,7 @@ export type VisitsApiQueryParams = { }>; }; -// @public (undocumented) +// @public export const visitsApiRef: ApiRef; // @public diff --git a/plugins/home/src/alpha.test.ts b/plugins/home/src/alpha.test.ts new file mode 100644 index 0000000000..caa7d45cf7 --- /dev/null +++ b/plugins/home/src/alpha.test.ts @@ -0,0 +1,111 @@ +/* + * 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 { mockApis } from '@backstage/test-utils'; +import homePlugin from './alpha'; +import { VisitsStorageApi, VisitsWebStorageApi } from './api'; + +// Mock localStorage for testing +const mockLocalStorage = new Map(); +Object.defineProperty(window, 'localStorage', { + value: { + getItem: (key: string) => mockLocalStorage.get(key) || null, + setItem: (key: string, value: string) => mockLocalStorage.set(key, value), + removeItem: (key: string) => mockLocalStorage.delete(key), + clear: () => mockLocalStorage.clear(), + }, +}); + +describe('Home Plugin Alpha', () => { + beforeEach(() => { + mockLocalStorage.clear(); + }); + + describe('Core Extensions (Always Enabled)', () => { + it('should export core home page extension', () => { + expect(homePlugin.getExtension('page:home')).toBeDefined(); + }); + + it('should export navigation item extension', () => { + expect(homePlugin.getExtension('nav-item:home')).toBeDefined(); + }); + }); + + describe('Optional Extensions (Disabled by Default)', () => { + it('should export visit tracking API extension', () => { + const visitTrackingExtension = homePlugin.getExtension('api:home/visits'); + expect(visitTrackingExtension).toBeDefined(); + }); + + it('should export visit listener extension', () => { + const visitListenerExtension = homePlugin.getExtension( + 'app-root-element:home/visit-listener', + ); + expect(visitListenerExtension).toBeDefined(); + }); + }); + + describe('API Implementation Classes', () => { + it('should create VisitsStorageApi with custom storage', () => { + const mockStorageApi = mockApis.storage(); + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/testuser', + }); + + const visitsApi = VisitsStorageApi.create({ + storageApi: mockStorageApi, + identityApi: mockIdentityApi, + }); + + expect(visitsApi).toBeInstanceOf(VisitsStorageApi); + }); + + it('should create localStorage fallback API when no custom storage is available', () => { + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/testuser', + }); + const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; + + // Test that VisitsWebStorageApi can be created without custom storage + const visitsApi = VisitsWebStorageApi.create({ + identityApi: mockIdentityApi, + errorApi: mockErrorApi, + }); + + // VisitsWebStorageApi.create returns a VisitsStorageApi instance + expect(visitsApi).toBeInstanceOf(VisitsStorageApi); + }); + }); + + describe('Plugin Structure', () => { + it('should have correct plugin metadata', () => { + expect(homePlugin.id).toBe('home'); + expect(homePlugin.routes.root).toBeDefined(); + }); + + it('should include all extensions in the correct order', () => { + // Core extensions (always enabled) + expect(homePlugin.getExtension('page:home')).toBeDefined(); + expect(homePlugin.getExtension('nav-item:home')).toBeDefined(); + + // Optional extensions (disabled by default) + expect(homePlugin.getExtension('api:home/visits')).toBeDefined(); + expect( + homePlugin.getExtension('app-root-element:home/visit-listener'), + ).toBeDefined(); + }); + }); +}); diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 6cad71d643..35a734c98d 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -14,30 +14,36 @@ * limitations under the License. */ +/** + * The home plugin for Backstage's new frontend system. + * + * @remarks + * This package provides the new frontend system implementation of the home plugin, + * which offers customizable home pages with widget support and optional visit tracking. + * + * @packageDocumentation + */ + import { coreExtensionData, - createExtensionDataRef, createExtensionInput, PageBlueprint, + NavItemBlueprint, createFrontendPlugin, createRouteRef, AppRootElementBlueprint, identityApiRef, storageApiRef, + errorApiRef, ApiBlueprint, } from '@backstage/frontend-plugin-api'; import { VisitListener } from './components/'; -import { visitsApiRef, VisitsStorageApi } from './api'; +import { visitsApiRef, VisitsStorageApi, VisitsWebStorageApi } from './api'; +import { titleExtensionDataRef } from '@backstage/plugin-home-react/alpha'; +import HomeIcon from '@material-ui/icons/Home'; const rootRouteRef = createRouteRef(); -/** - * @alpha - */ -export const titleExtensionDataRef = createExtensionDataRef().with({ - id: 'title', -}); - const homePage = PageBlueprint.makeWithOverrides({ inputs: { props: createExtensionInput( @@ -68,6 +74,7 @@ const homePage = PageBlueprint.makeWithOverrides({ const visitListenerAppRootElement = AppRootElementBlueprint.make({ name: 'visit-listener', + disabled: true, params: { element: , }, @@ -75,28 +82,58 @@ const visitListenerAppRootElement = AppRootElementBlueprint.make({ const visitsApi = ApiBlueprint.make({ name: 'visits', + disabled: true, params: defineParams => defineParams({ api: visitsApiRef, deps: { storageApi: storageApiRef, identityApi: identityApiRef, + errorApi: errorApiRef, + }, + factory: ({ storageApi, identityApi, errorApi }) => { + // Smart fallback: use custom storage API if available, otherwise localStorage + if (storageApi) { + return VisitsStorageApi.create({ storageApi, identityApi }); + } + return VisitsWebStorageApi.create({ identityApi, errorApi }); }, - factory: ({ storageApi, identityApi }) => - VisitsStorageApi.create({ storageApi, identityApi }), }), }); +const homeNavItem = NavItemBlueprint.make({ + params: { + title: 'Home', + routeRef: rootRouteRef, + icon: HomeIcon, + }, +}); + /** + * Home plugin for the new frontend system. + * + * Provides core homepage functionality with optional visit tracking extensions. + * Visit tracking extensions are disabled by default and can be enabled via app-config.yaml. + * * @alpha */ export default createFrontendPlugin({ pluginId: 'home', info: { packageJson: () => import('../package.json') }, - extensions: [homePage, visitsApi, visitListenerAppRootElement], + extensions: [homePage, homeNavItem, visitsApi, visitListenerAppRootElement], routes: { root: rootRouteRef, }, }); export { homeTranslationRef } from './translation'; +export { + HomepageBlueprint, + type HomepageBlueprintParams, + type HomepageTemplateProps, + type HomepageGridProps, +} from './alpha/HomepageBlueprint'; +export { + type LayoutConfiguration, + type Breakpoint, +} from './components/CustomHomepage/types'; diff --git a/plugins/home/src/alpha/HomepageBlueprint.tsx b/plugins/home/src/alpha/HomepageBlueprint.tsx new file mode 100644 index 0000000000..befc5b74fd --- /dev/null +++ b/plugins/home/src/alpha/HomepageBlueprint.tsx @@ -0,0 +1,115 @@ +/* + * 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 { compatWrapper } from '@backstage/core-compat-api'; +import { Content } from '@backstage/core-components'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionInput, + ExtensionBoundary, +} from '@backstage/frontend-plugin-api'; +import { Fragment, type ReactElement, type ReactNode } from 'react'; +import { CustomHomepageGrid } from '../components'; +import type { CustomHomepageGridProps } from '../components'; +import { titleExtensionDataRef } from '@backstage/plugin-home-react/alpha'; + +/** + * Arguments provided to the homepage renderer. + * + * @alpha + */ +export interface HomepageTemplateProps { + /** + * React elements built from the installed homepage widgets. + */ + widgets: ReactNode[]; + /** + * A element that renders the widgets using the provided props. + */ + grid: ReactElement; +} + +/** + * Parameters for creating a homepage extension. + * + * @alpha + */ +export interface HomepageBlueprintParams { + /** + * Optional title used by the home page when rendered through the new frontend system. + */ + title?: string; + /** + * Props forwarded to . The `children` prop is managed by the blueprint. + */ + grid?: Omit; + /** + * Allows supplying a custom renderer for the homepage. Receives the generated widgets as well + * as a element configured with the provided props. + */ + render?: (props: HomepageTemplateProps) => ReactElement; +} + +const DEFAULT_ATTACH_POINT = Object.freeze({ + id: 'page:home', + input: 'props', +}); + +/** + * Blueprint that composes a home page based on installed widgets. + * + * @alpha + */ +export const HomepageBlueprint = createExtensionBlueprint({ + kind: 'home-page', + attachTo: DEFAULT_ATTACH_POINT, + output: [coreExtensionData.reactElement, titleExtensionDataRef.optional()], + inputs: { + widgets: createExtensionInput([coreExtensionData.reactElement]), + }, + *factory(params: HomepageBlueprintParams = {}, { inputs, node }) { + const widgetOutputs = inputs.widgets ?? []; + const widgetElements = widgetOutputs.map((widget, index) => ( + + {widget.get(coreExtensionData.reactElement)} + + )); + + const gridElement = ( + + {widgetElements} + + ); + + const renderedElement = params.render?.({ + widgets: widgetElements, + grid: gridElement, + }) ?? {gridElement}; + + yield coreExtensionData.reactElement( + + {compatWrapper(renderedElement)} + , + ); + + if (params.title) { + yield titleExtensionDataRef(params.title); + } + }, +}); + +export type { CustomHomepageGridProps as HomepageGridProps } from '../components'; diff --git a/plugins/home/src/api/VisitsApi.ts b/plugins/home/src/api/VisitsApi.ts index 5cbd65fab3..3345128e08 100644 --- a/plugins/home/src/api/VisitsApi.ts +++ b/plugins/home/src/api/VisitsApi.ts @@ -146,7 +146,12 @@ export interface VisitsApi { ): Promise> | Record; } -/** @public */ +/** + * API reference for the visits tracking service. + * Provides functionality to track and retrieve user page visit history. + * + * @public + */ export const visitsApiRef = createApiRef({ id: 'homepage.visits', }); diff --git a/plugins/home/src/translation.ts b/plugins/home/src/translation.ts index 6e8ad2c511..480e2850b9 100644 --- a/plugins/home/src/translation.ts +++ b/plugins/home/src/translation.ts @@ -16,6 +16,9 @@ import { createTranslationRef } from '@backstage/frontend-plugin-api'; /** + * Translation reference for the home plugin. + * Contains localized text strings for home page components and widgets. + * * @alpha */ export const homeTranslationRef = createTranslationRef({ diff --git a/yarn.lock b/yarn.lock index a057a759bf..1e15e4020b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5897,6 +5897,7 @@ __metadata: resolution: "@backstage/plugin-home-react@workspace:plugins/home-react" dependencies: "@backstage/cli": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" @@ -5931,7 +5932,9 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-defaults": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-home-react": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -30991,6 +30994,7 @@ __metadata: "@backstage/plugin-catalog-unprocessed-entities": "workspace:^" "@backstage/plugin-devtools": "workspace:^" "@backstage/plugin-home": "workspace:^" + "@backstage/plugin-home-react": "workspace:^" "@backstage/plugin-kubernetes": "workspace:^" "@backstage/plugin-kubernetes-cluster": "workspace:^" "@backstage/plugin-notifications": "workspace:^" From 66f29d2921584e299a94e2cbfab6f950850392a1 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sat, 25 Oct 2025 21:54:35 -0700 Subject: [PATCH 02/12] Update to use coreExtensionsData.title Signed-off-by: Adam Kunicki --- packages/app-next/src/App.tsx | 5 ++-- plugins/home-react/report-alpha.api.md | 7 ------ plugins/home-react/src/alpha.ts | 1 - plugins/home-react/src/alpha/dataRefs.ts | 26 -------------------- plugins/home/report-alpha.api.md | 4 +-- plugins/home/src/alpha.tsx | 11 ++++----- plugins/home/src/alpha/HomepageBlueprint.tsx | 5 ++-- 7 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 plugins/home-react/src/alpha/dataRefs.ts diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index df63ccd5c5..13bec94c98 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -19,7 +19,6 @@ import { pagesPlugin } from './examples/pagesPlugin'; import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin from '@backstage/plugin-home/alpha'; -import { titleExtensionDataRef } from '@backstage/plugin-home-react/alpha'; import { coreExtensionData, @@ -103,11 +102,11 @@ const customHomePageModule = createFrontendModule({ createExtension({ name: 'my-home-page', attachTo: { id: 'page:home', input: 'props' }, - output: [coreExtensionData.reactElement, titleExtensionDataRef], + output: [coreExtensionData.reactElement, coreExtensionData.title], factory() { return [ coreExtensionData.reactElement(homePage), - titleExtensionDataRef('just a title'), + coreExtensionData.title('just a title'), ]; }, }), diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index 1689a91f73..acc8f629f0 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -83,13 +83,6 @@ export const homeReactTranslationRef: TranslationRef< } >; -// @alpha -export const titleExtensionDataRef: ConfigurableExtensionDataRef< - string, - 'title', - {} ->; - // @alpha export const widgetMetadataRef: ConfigurableExtensionDataRef< { diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index b72ba1972b..88f318a3f8 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -24,7 +24,6 @@ * @packageDocumentation */ export { homeReactTranslationRef } from './translation'; -export { titleExtensionDataRef } from './alpha/dataRefs'; export { HomepageWidgetBlueprint, widgetMetadataRef, diff --git a/plugins/home-react/src/alpha/dataRefs.ts b/plugins/home-react/src/alpha/dataRefs.ts deleted file mode 100644 index 5c15f9b527..0000000000 --- a/plugins/home-react/src/alpha/dataRefs.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { createExtensionDataRef } from '@backstage/frontend-plugin-api'; - -/** - * Title data supplied to the home page extension when composing via the new frontend system. - * - * @alpha - */ -export const titleExtensionDataRef = createExtensionDataRef().with({ - id: 'title', -}); diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 7de07f408c..d84d3e1ce5 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -106,7 +106,7 @@ const _default: OverridableFrontendPlugin< > | ConfigurableExtensionDataRef< string, - 'title', + 'core.title', { optional: true; } @@ -139,7 +139,7 @@ export const HomepageBlueprint: ExtensionBlueprint<{ | ExtensionDataRef | ExtensionDataRef< string, - 'title', + 'core.title', { optional: true; } diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 35a734c98d..3f34b4846d 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -39,7 +39,6 @@ import { } from '@backstage/frontend-plugin-api'; import { VisitListener } from './components/'; import { visitsApiRef, VisitsStorageApi, VisitsWebStorageApi } from './api'; -import { titleExtensionDataRef } from '@backstage/plugin-home-react/alpha'; import HomeIcon from '@material-ui/icons/Home'; const rootRouteRef = createRouteRef(); @@ -49,7 +48,7 @@ const homePage = PageBlueprint.makeWithOverrides({ props: createExtensionInput( [ coreExtensionData.reactElement.optional(), - titleExtensionDataRef.optional(), + coreExtensionData.title.optional(), ], { singleton: true, @@ -63,10 +62,10 @@ const homePage = PageBlueprint.makeWithOverrides({ routeRef: rootRouteRef, loader: () => import('./components/').then(m => ( - + , )), }); }, diff --git a/plugins/home/src/alpha/HomepageBlueprint.tsx b/plugins/home/src/alpha/HomepageBlueprint.tsx index befc5b74fd..8eabae2153 100644 --- a/plugins/home/src/alpha/HomepageBlueprint.tsx +++ b/plugins/home/src/alpha/HomepageBlueprint.tsx @@ -25,7 +25,6 @@ import { import { Fragment, type ReactElement, type ReactNode } from 'react'; import { CustomHomepageGrid } from '../components'; import type { CustomHomepageGridProps } from '../components'; -import { titleExtensionDataRef } from '@backstage/plugin-home-react/alpha'; /** * Arguments provided to the homepage renderer. @@ -77,7 +76,7 @@ const DEFAULT_ATTACH_POINT = Object.freeze({ export const HomepageBlueprint = createExtensionBlueprint({ kind: 'home-page', attachTo: DEFAULT_ATTACH_POINT, - output: [coreExtensionData.reactElement, titleExtensionDataRef.optional()], + output: [coreExtensionData.reactElement, coreExtensionData.title.optional()], inputs: { widgets: createExtensionInput([coreExtensionData.reactElement]), }, @@ -107,7 +106,7 @@ export const HomepageBlueprint = createExtensionBlueprint({ ); if (params.title) { - yield titleExtensionDataRef(params.title); + yield coreExtensionData.title(params.title); } }, }); From aeae90c96921f0c307c5fc46e892538e68394539 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sun, 26 Oct 2025 19:25:02 -0700 Subject: [PATCH 03/12] refactor(home): bundle widget data into single extension ref Following FormFieldBlueprint pattern, bundle component and metadata into a single comprehensive homePageWidgetDataRef instead of outputting them separately. This addresses review feedback about outputting data that nothing consumes. - Create homePageWidgetDataRef in plugin-home-react - Update HomepageWidgetBlueprint to output bundled data - Update HomepageBlueprint to consume bundled data and extract component - Remove unused widgetMetadataRef export Signed-off-by: Adam Kunicki --- plugins/home-react/src/alpha.ts | 5 +- .../blueprints/HomepageWidgetBlueprint.tsx | 24 ++----- plugins/home-react/src/alpha/dataRefs.ts | 66 +++++++++++++++++++ plugins/home/src/alpha/HomepageBlueprint.tsx | 5 +- 4 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 plugins/home-react/src/alpha/dataRefs.ts diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 88f318a3f8..2766ef83a9 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -26,7 +26,10 @@ export { homeReactTranslationRef } from './translation'; export { HomepageWidgetBlueprint, - widgetMetadataRef, type HomepageWidgetBlueprintParams, } from './alpha/blueprints/HomepageWidgetBlueprint'; +export { + homePageWidgetDataRef, + type HomePageWidgetData, +} from './alpha/dataRefs'; export type { ComponentParts, CardLayout, CardSettings } from './extensions'; diff --git a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx index 553a6b3249..0cb898a5f8 100644 --- a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx +++ b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx @@ -17,9 +17,7 @@ import { lazy, ReactElement } from 'react'; import { compatWrapper } from '@backstage/core-compat-api'; import { - coreExtensionData, createExtensionBlueprint, - createExtensionDataRef, ExtensionBoundary, } from '@backstage/frontend-plugin-api'; import { @@ -29,6 +27,7 @@ import { CardSettings, ComponentParts, } from '../../extensions'; +import { homePageWidgetDataRef } from '../dataRefs'; /** @alpha */ export interface HomepageWidgetBlueprintParams { @@ -68,18 +67,6 @@ const DEFAULT_WIDGET_ATTACH_POINT = { input: 'widgets', } as const; -/** - * Extension data ref for widget metadata. - * @alpha - */ -export const widgetMetadataRef = createExtensionDataRef<{ - name?: string; - title?: string; - description?: string; - layout?: CardLayout; - settings?: CardSettings; -}>().with({ id: 'home.widget.metadata' }); - /** * Creates widgets that can be installed into the home page grid. * @@ -88,7 +75,7 @@ export const widgetMetadataRef = createExtensionDataRef<{ export const HomepageWidgetBlueprint = createExtensionBlueprint({ kind: 'home-widget', attachTo: DEFAULT_WIDGET_ATTACH_POINT, - output: [coreExtensionData.reactElement, widgetMetadataRef], + output: [homePageWidgetDataRef], *factory(params: HomepageWidgetBlueprintParams, { node }) { const isCustomizable = params.settings?.schema !== undefined; const LazyCard = lazy(() => @@ -113,11 +100,8 @@ export const HomepageWidgetBlueprint = createExtensionBlueprint({ , ); - yield coreExtensionData.reactElement( - , - ); - - yield widgetMetadataRef({ + yield homePageWidgetDataRef({ + component: , name: params.name, title: params.title, description: params.description, diff --git a/plugins/home-react/src/alpha/dataRefs.ts b/plugins/home-react/src/alpha/dataRefs.ts new file mode 100644 index 0000000000..00b221994f --- /dev/null +++ b/plugins/home-react/src/alpha/dataRefs.ts @@ -0,0 +1,66 @@ +/* + * 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 { createExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ReactElement } from 'react'; +import type { CardLayout, CardSettings } from '../extensions'; + +/** + * Extension data for homepage widgets, bundling the rendered component + * with its metadata. + * + * @alpha + */ +export interface HomePageWidgetData { + /** + * The rendered widget component (typically a card with header, content, etc.) + */ + component: ReactElement; + /** + * Optional name identifier for the widget + */ + name?: string; + /** + * Optional title displayed in the widget header + */ + title?: string; + /** + * Optional description shown in widget catalogs or configuration UIs + */ + description?: string; + /** + * Optional layout hints for positioning and sizing + */ + layout?: CardLayout; + /** + * Optional settings schema for widget configuration + */ + settings?: CardSettings; +} + +/** + * Extension data ref for homepage widgets. + * + * This follows the pattern from FormFieldBlueprint, bundling the component + * and metadata into a single comprehensive data ref rather than outputting + * them separately. + * + * @alpha + */ +export const homePageWidgetDataRef = + createExtensionDataRef().with({ + id: 'home.widget.data', + }); diff --git a/plugins/home/src/alpha/HomepageBlueprint.tsx b/plugins/home/src/alpha/HomepageBlueprint.tsx index 8eabae2153..b72ff4d587 100644 --- a/plugins/home/src/alpha/HomepageBlueprint.tsx +++ b/plugins/home/src/alpha/HomepageBlueprint.tsx @@ -25,6 +25,7 @@ import { import { Fragment, type ReactElement, type ReactNode } from 'react'; import { CustomHomepageGrid } from '../components'; import type { CustomHomepageGridProps } from '../components'; +import { homePageWidgetDataRef } from '@backstage/plugin-home-react/alpha'; /** * Arguments provided to the homepage renderer. @@ -78,13 +79,13 @@ export const HomepageBlueprint = createExtensionBlueprint({ attachTo: DEFAULT_ATTACH_POINT, output: [coreExtensionData.reactElement, coreExtensionData.title.optional()], inputs: { - widgets: createExtensionInput([coreExtensionData.reactElement]), + widgets: createExtensionInput([homePageWidgetDataRef]), }, *factory(params: HomepageBlueprintParams = {}, { inputs, node }) { const widgetOutputs = inputs.widgets ?? []; const widgetElements = widgetOutputs.map((widget, index) => ( - {widget.get(coreExtensionData.reactElement)} + {widget.get(homePageWidgetDataRef).component} )); From d79fb2f65d88fc6d12e9c462199fe1854365318a Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Thu, 6 Nov 2025 20:54:06 -0800 Subject: [PATCH 04/12] chore(home): update api reports Signed-off-by: Adam Kunicki --- plugins/home-react/report-alpha.api.md | 46 +++++++++++--------------- plugins/home/report-alpha.api.md | 3 +- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index acc8f629f0..9e30c78bc9 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -6,7 +6,7 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { JSX as JSX_2 } from 'react'; +import { ReactElement } from 'react'; import { RJSFSchema } from '@rjsf/utils'; import { TranslationRef } from '@backstage/frontend-plugin-api'; import { UiSchema } from '@rjsf/utils'; @@ -43,19 +43,7 @@ export type ComponentParts = { export const HomepageWidgetBlueprint: ExtensionBlueprint<{ kind: 'home-widget'; params: HomepageWidgetBlueprintParams; - output: - | ExtensionDataRef - | ExtensionDataRef< - { - name?: string; - title?: string; - description?: string; - layout?: CardLayout; - settings?: CardSettings; - }, - 'home.widget.metadata', - {} - >; + output: ExtensionDataRef; inputs: {}; config: {}; configInput: {}; @@ -73,6 +61,23 @@ export interface HomepageWidgetBlueprintParams { title?: string; } +// @alpha +export interface HomePageWidgetData { + component: ReactElement; + description?: string; + layout?: CardLayout; + name?: string; + settings?: CardSettings; + title?: string; +} + +// @alpha +export const homePageWidgetDataRef: ConfigurableExtensionDataRef< + HomePageWidgetData, + 'home.widget.data', + {} +>; + // @alpha export const homeReactTranslationRef: TranslationRef< 'home-react', @@ -82,17 +87,4 @@ export const homeReactTranslationRef: TranslationRef< readonly 'cardExtension.settingsButtonTitle': 'Settings'; } >; - -// @alpha -export const widgetMetadataRef: ConfigurableExtensionDataRef< - { - name?: string; - title?: string; - description?: string; - layout?: CardLayout; - settings?: CardSettings; - }, - 'home.widget.metadata', - {} ->; ``` diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index d84d3e1ce5..6ae6e44bdb 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -12,6 +12,7 @@ import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { HomePageWidgetData } from '@backstage/plugin-home-react/alpha'; import { IconComponent } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; @@ -146,7 +147,7 @@ export const HomepageBlueprint: ExtensionBlueprint<{ >; inputs: { widgets: ExtensionInput< - ConfigurableExtensionDataRef, + ConfigurableExtensionDataRef, { singleton: false; optional: false; From e7e9fc638688d937d6a06037691d9ed672ff0062 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Thu, 29 Jan 2026 18:48:19 -0800 Subject: [PATCH 05/12] fix(home): restore CI by fixing loader and exposing widget dataRef Signed-off-by: Adam Kunicki --- plugins/home-react/report-alpha.api.md | 8 +++++++- .../src/alpha/blueprints/HomepageWidgetBlueprint.tsx | 3 +++ plugins/home/src/alpha.tsx | 8 ++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index 9e30c78bc9..9d21b69733 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -47,7 +47,13 @@ export const HomepageWidgetBlueprint: ExtensionBlueprint<{ inputs: {}; config: {}; configInput: {}; - dataRefs: never; + dataRefs: { + widget: ConfigurableExtensionDataRef< + HomePageWidgetData, + 'home.widget.data', + {} + >; + }; }>; // @alpha (undocumented) diff --git a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx index 0cb898a5f8..6e4c6f3c92 100644 --- a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx +++ b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx @@ -75,6 +75,9 @@ const DEFAULT_WIDGET_ATTACH_POINT = { export const HomepageWidgetBlueprint = createExtensionBlueprint({ kind: 'home-widget', attachTo: DEFAULT_WIDGET_ATTACH_POINT, + dataRefs: { + widget: homePageWidgetDataRef, + }, output: [homePageWidgetDataRef], *factory(params: HomepageWidgetBlueprintParams, { node }) { const isCustomizable = params.settings?.schema !== undefined; diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 3f34b4846d..9db31259dd 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -62,10 +62,10 @@ const homePage = PageBlueprint.makeWithOverrides({ routeRef: rootRouteRef, loader: () => import('./components/').then(m => ( - , + )), }); }, From 06e0a2cb22a8736270f786c2c12cbf837cefb46d Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Fri, 30 Jan 2026 08:38:30 -0800 Subject: [PATCH 06/12] fix lint issues/ci Signed-off-by: Adam Kunicki --- plugins/home/package.json | 1 + plugins/home/report-alpha.api.md | 3 ++- .../src/homePageComponents/QuickStart/QuickStart.stories.tsx | 5 +++-- yarn.lock | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/home/package.json b/plugins/home/package.json index f8e2162fbd..d08879a8b3 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -60,6 +60,7 @@ "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/core-app-api": "workspace:^", + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 6ae6e44bdb..416b6f6f40 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -13,7 +13,7 @@ import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { HomePageWidgetData } from '@backstage/plugin-home-react/alpha'; -import { IconComponent } from '@backstage/core-plugin-api'; +import { IconComponent } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; @@ -151,6 +151,7 @@ export const HomepageBlueprint: ExtensionBlueprint<{ { singleton: false; optional: false; + internal: false; } >; }; diff --git a/plugins/home/src/homePageComponents/QuickStart/QuickStart.stories.tsx b/plugins/home/src/homePageComponents/QuickStart/QuickStart.stories.tsx index 0f83f7915e..a32c0da1e3 100644 --- a/plugins/home/src/homePageComponents/QuickStart/QuickStart.stories.tsx +++ b/plugins/home/src/homePageComponents/QuickStart/QuickStart.stories.tsx @@ -18,6 +18,7 @@ import { QuickStartCard } from '../../plugin'; import { ComponentType, PropsWithChildren } from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import ContentImage from './static/backstageSystemModel.png'; @@ -64,11 +65,11 @@ export const Customized = () => { } cardDescription="Backstage system model will help you create new entities" additionalContent={ -

+ This is a custom description for the Quick Start card. It can be used to provide additional information or context about the Quick Start process. -

+ } /> diff --git a/yarn.lock b/yarn.lock index 1e15e4020b..ba79164b1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5929,6 +5929,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/core-app-api": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" From 03fc07adb73d15808ba2dc567606348fff699966 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sat, 7 Feb 2026 04:50:04 -0800 Subject: [PATCH 07/12] refactor(home): restructure extension architecture to match catalog pattern Reorganize the home plugin's new frontend system extensions to follow the same pattern as the catalog plugin's EntityContentLayoutBlueprint. Key changes: - Widgets now attach directly to page:home instead of through an intermediate HomepageBlueprint extension - Add HomePageLayoutBlueprint in home-react for custom layout creation, with a built-in default layout as fallback - Remove compatWrapper from HomepageWidgetBlueprint (consumer responsibility, reduced need with NFS support in old system) - Remove HomepageCompositionRoot usage (old system artifact) - Delete HomepageBlueprint (replaced by HomePageLayoutBlueprint) - Update app-next example and dev harness to use new pattern Signed-off-by: Adam Kunicki --- packages/app-next/src/App.tsx | 24 ++-- plugins/home-react/report-alpha.api.md | 39 ++++++ plugins/home-react/src/alpha.ts | 6 + .../blueprints/HomePageLayoutBlueprint.tsx | 64 ++++++++++ .../blueprints/HomepageWidgetBlueprint.tsx | 12 +- plugins/home-react/src/alpha/dataRefs.ts | 29 ++++- plugins/home/dev/index.tsx | 56 +++++---- plugins/home/report-alpha.api.md | 103 ++++------------ plugins/home/src/alpha.tsx | 67 ++++++---- .../home/src/alpha/DefaultHomePageLayout.tsx | 39 ++++++ plugins/home/src/alpha/HomepageBlueprint.tsx | 115 ------------------ 11 files changed, 289 insertions(+), 265 deletions(-) create mode 100644 plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx create mode 100644 plugins/home/src/alpha/DefaultHomePageLayout.tsx delete mode 100644 plugins/home/src/alpha/HomepageBlueprint.tsx diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 13bec94c98..2674493b8e 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -20,11 +20,8 @@ import notFoundErrorPage from './examples/notFoundErrorPageExtension'; import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin from '@backstage/plugin-home/alpha'; -import { - coreExtensionData, - createExtension, - createFrontendModule, -} from '@backstage/frontend-plugin-api'; +import { createFrontendModule } from '@backstage/frontend-plugin-api'; +import { HomePageLayoutBlueprint } from '@backstage/plugin-home-react/alpha'; import { techdocsPlugin, TechDocsIndexPage, @@ -99,15 +96,14 @@ const convertedTechdocsPlugin = convertLegacyPlugin(techdocsPlugin, { const customHomePageModule = createFrontendModule({ pluginId: 'home', extensions: [ - createExtension({ - name: 'my-home-page', - attachTo: { id: 'page:home', input: 'props' }, - output: [coreExtensionData.reactElement, coreExtensionData.title], - factory() { - return [ - coreExtensionData.reactElement(homePage), - coreExtensionData.title('just a title'), - ]; + HomePageLayoutBlueprint.make({ + params: { + loader: async () => + // This is a legacy-style home page that renders static content. + // In production, you'd use the widgets prop to render dynamic widgets. + function LegacyHomePageLayout() { + return homePage; + }, }, }), ], diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index 9d21b69733..0fc6360e35 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -6,6 +6,7 @@ import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { ReactElement } from 'react'; import { RJSFSchema } from '@rjsf/utils'; import { TranslationRef } from '@backstage/frontend-plugin-api'; @@ -39,6 +40,44 @@ export type ComponentParts = { ContextProvider?: (props: any) => JSX.Element; }; +// @alpha +export const HomePageLayoutBlueprint: ExtensionBlueprint<{ + kind: 'home-page-layout'; + params: HomePageLayoutBlueprintParams; + output: ExtensionDataRef< + (props: HomePageLayoutProps) => JSX_2.Element, + 'home.layout.component', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + component: ConfigurableExtensionDataRef< + (props: HomePageLayoutProps) => JSX_2.Element, + 'home.layout.component', + {} + >; + }; +}>; + +// @alpha +export interface HomePageLayoutBlueprintParams { + loader: () => Promise<(props: HomePageLayoutProps) => JSX_2.Element>; +} + +// @alpha +export const homePageLayoutComponentDataRef: ConfigurableExtensionDataRef< + (props: HomePageLayoutProps) => JSX_2.Element, + 'home.layout.component', + {} +>; + +// @alpha +export interface HomePageLayoutProps { + widgets: Array; +} + // @alpha export const HomepageWidgetBlueprint: ExtensionBlueprint<{ kind: 'home-widget'; diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 2766ef83a9..1a3432e02e 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -28,8 +28,14 @@ export { HomepageWidgetBlueprint, type HomepageWidgetBlueprintParams, } from './alpha/blueprints/HomepageWidgetBlueprint'; +export { + HomePageLayoutBlueprint, + type HomePageLayoutBlueprintParams, +} from './alpha/blueprints/HomePageLayoutBlueprint'; export { homePageWidgetDataRef, type HomePageWidgetData, + homePageLayoutComponentDataRef, + type HomePageLayoutProps, } from './alpha/dataRefs'; export type { ComponentParts, CardLayout, CardSettings } from './extensions'; diff --git a/plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx new file mode 100644 index 0000000000..c1d4e94ffb --- /dev/null +++ b/plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx @@ -0,0 +1,64 @@ +/* + * 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 { JSX } from 'react'; +import { + createExtensionBlueprint, + ExtensionBoundary, +} from '@backstage/frontend-plugin-api'; +import { + homePageLayoutComponentDataRef, + HomePageLayoutProps, +} from '../dataRefs'; + +/** + * Parameters for creating a home page layout extension. + * + * @alpha + */ +export interface HomePageLayoutBlueprintParams { + /** + * Async loader that returns the layout component. + * The component receives the collected widgets and renders them. + */ + loader: () => Promise<(props: HomePageLayoutProps) => JSX.Element>; +} + +/** + * Blueprint for creating custom home page layouts. + * + * A layout receives the list of installed widgets and is responsible for + * arranging them on the home page. This follows the same pattern as + * `EntityContentLayoutBlueprint` in the catalog plugin. + * + * If no layout extension is installed, the home page uses a built-in default. + * Users can install their own layout to customize how widgets are arranged. + * + * @alpha + */ +export const HomePageLayoutBlueprint = createExtensionBlueprint({ + kind: 'home-page-layout', + attachTo: { id: 'page:home', input: 'layouts' }, + output: [homePageLayoutComponentDataRef], + dataRefs: { + component: homePageLayoutComponentDataRef, + }, + *factory({ loader }: HomePageLayoutBlueprintParams, { node }) { + yield homePageLayoutComponentDataRef( + ExtensionBoundary.lazyComponent(node, loader), + ); + }, +}); diff --git a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx index 6e4c6f3c92..e3375f2cb9 100644 --- a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx +++ b/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx @@ -15,7 +15,6 @@ */ import { lazy, ReactElement } from 'react'; -import { compatWrapper } from '@backstage/core-compat-api'; import { createExtensionBlueprint, ExtensionBoundary, @@ -96,12 +95,11 @@ export const HomepageWidgetBlueprint = createExtensionBlueprint({ const Widget = ( props: CardExtensionProps>, - ): ReactElement => - compatWrapper( - - - , - ); + ): ReactElement => ( + + + + ); yield homePageWidgetDataRef({ component: , diff --git a/plugins/home-react/src/alpha/dataRefs.ts b/plugins/home-react/src/alpha/dataRefs.ts index 00b221994f..2fc6ee30fa 100644 --- a/plugins/home-react/src/alpha/dataRefs.ts +++ b/plugins/home-react/src/alpha/dataRefs.ts @@ -15,7 +15,7 @@ */ import { createExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ReactElement } from 'react'; +import { JSX, ReactElement } from 'react'; import type { CardLayout, CardSettings } from '../extensions'; /** @@ -64,3 +64,30 @@ export const homePageWidgetDataRef = createExtensionDataRef().with({ id: 'home.widget.data', }); + +/** + * Props provided to a home page layout component. + * + * @alpha + */ +export interface HomePageLayoutProps { + /** + * The list of widget elements and metadata to render on the home page. + */ + widgets: Array; +} + +/** + * Extension data ref for home page layout components. + * + * A layout receives the collected widgets and is responsible for arranging + * them on the home page. This follows the same pattern as + * EntityContentLayoutBlueprint in the catalog plugin. + * + * @alpha + */ +export const homePageLayoutComponentDataRef = createExtensionDataRef< + (props: HomePageLayoutProps) => JSX.Element +>().with({ + id: 'home.layout.component', +}); diff --git a/plugins/home/dev/index.tsx b/plugins/home/dev/index.tsx index edbcdc574b..ce292c4ee5 100644 --- a/plugins/home/dev/index.tsx +++ b/plugins/home/dev/index.tsx @@ -21,17 +21,20 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import catalogPlugin from '@backstage/plugin-catalog/alpha'; import HomeIcon from '@material-ui/icons/Home'; import ReactDOM from 'react-dom/client'; +import { Fragment } from 'react'; import { ApiBlueprint, createFrontendModule, } from '@backstage/frontend-plugin-api'; -import { HomepageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; +import { + HomepageWidgetBlueprint, + HomePageLayoutBlueprint, +} from '@backstage/plugin-home-react/alpha'; import { HeaderWorldClock, WelcomeTitle, type ClockConfig } from '../src'; -import homePlugin, { - HomepageBlueprint, - type HomepageGridProps, -} from '../src/alpha'; +import homePlugin from '../src/alpha'; +import { CustomHomepageGrid } from '../src/components'; +import type { LayoutConfiguration } from '../src/components/CustomHomepage/types'; const clockConfigs: ClockConfig[] = [ { label: 'NYC', timeZone: 'America/New_York' }, @@ -46,7 +49,7 @@ const timeFormat: Intl.DateTimeFormatOptions = { hour12: false, }; -const defaultGridConfig: NonNullable = [ +const defaultGridConfig: LayoutConfiguration[] = [ { component: 'HomePageToolkit', x: 0, @@ -72,23 +75,30 @@ const defaultGridConfig: NonNullable = [ }, ]; -const homePage = HomepageBlueprint.make({ +const homePageLayout = HomePageLayoutBlueprint.make({ params: { - title: 'Home', - grid: { - config: defaultGridConfig, - }, - render: ({ grid }) => ( - -
} pageTitleOverride="Home"> - -
- {grid} -
- ), + loader: async () => + function CustomHomePageLayout({ widgets }) { + return ( + +
} pageTitleOverride="Home"> + +
+ + + {widgets.map((widget, index) => ( + + {widget.component} + + ))} + + +
+ ); + }, }, }); @@ -163,7 +173,7 @@ const homePageRandomJokeWidget = HomepageWidgetBlueprint.make({ const homeDevModule = createFrontendModule({ pluginId: 'home', extensions: [ - homePage, + homePageLayout, homePageToolkitWidget, homePageStarredEntitiesWidget, homePageRandomJokeWidget, diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 416b6f6f40..5214736c1c 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -7,18 +7,16 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { CSSProperties } from 'react'; -import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { HomePageLayoutProps } from '@backstage/plugin-home-react/alpha'; import { HomePageWidgetData } from '@backstage/plugin-home-react/alpha'; import { IconComponent } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; import { ReactElement } from 'react'; -import { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; @@ -97,24 +95,27 @@ const _default: OverridableFrontendPlugin< } >; inputs: { - props: ExtensionInput< - | ConfigurableExtensionDataRef< - JSX_2.Element, - 'core.reactElement', - { - optional: true; - } - > - | ConfigurableExtensionDataRef< - string, - 'core.title', - { - optional: true; - } - >, + widgets: ExtensionInput< + ConfigurableExtensionDataRef< + HomePageWidgetData, + 'home.widget.data', + {} + >, { - singleton: true; - optional: true; + singleton: false; + optional: false; + internal: false; + } + >; + layouts: ExtensionInput< + ConfigurableExtensionDataRef< + (props: HomePageLayoutProps) => JSX_2.Element, + 'home.layout.component', + {} + >, + { + singleton: false; + optional: false; internal: false; } >; @@ -132,69 +133,10 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha -export const HomepageBlueprint: ExtensionBlueprint<{ - kind: 'home-page'; - params: HomepageBlueprintParams; - output: - | ExtensionDataRef - | ExtensionDataRef< - string, - 'core.title', - { - optional: true; - } - >; - inputs: { - widgets: ExtensionInput< - ConfigurableExtensionDataRef, - { - singleton: false; - optional: false; - internal: false; - } - >; - }; - config: {}; - configInput: {}; - dataRefs: never; -}>; - -// @alpha -export interface HomepageBlueprintParams { - grid?: Omit; - render?: (props: HomepageTemplateProps) => ReactElement; - title?: string; -} - -// @public -export type HomepageGridProps = { - children?: ReactNode; - config?: LayoutConfiguration[]; - title?: string; - rowHeight?: number; - breakpoints?: Record; - cols?: Record; - containerPadding?: [number, number] | Record; - containerMargin?: [number, number] | Record; - maxRows?: number; - style?: CSSProperties; - compactType?: 'vertical' | 'horizontal' | null; - allowOverlap?: boolean; - preventCollision?: boolean; -}; - -// @alpha -export interface HomepageTemplateProps { - grid: ReactElement; - widgets: ReactNode[]; -} - // @alpha export const homeTranslationRef: TranslationRef< 'home', { - readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!'; readonly 'addWidgetDialog.title': 'Add new widget to dashboard'; readonly 'customHomepageButtons.cancel': 'Cancel'; readonly 'customHomepageButtons.clearAll': 'Clear all'; @@ -203,10 +145,10 @@ export const homeTranslationRef: TranslationRef< readonly 'customHomepageButtons.addWidget': 'Add widget'; readonly 'customHomepageButtons.save': 'Save'; readonly 'customHomepage.noWidgets': "No widgets added. Start by clicking the 'Add widget' button."; - readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel'; readonly 'widgetSettingsOverlay.editSettingsTooptip': 'Edit settings'; readonly 'widgetSettingsOverlay.deleteWidgetTooltip': 'Delete widget'; readonly 'widgetSettingsOverlay.submitButtonTitle': 'Submit'; + readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel'; readonly 'starredEntityListItem.removeFavoriteEntityTitle': 'Remove entity from favorites'; readonly 'visitList.empty.title': 'There are no visits to show yet.'; readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.'; @@ -214,6 +156,7 @@ export const homeTranslationRef: TranslationRef< readonly 'quickStart.title': 'Onboarding'; readonly 'quickStart.description': 'Get started with Backstage'; readonly 'quickStart.learnMoreLinkTitle': 'Learn more'; + readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!'; readonly 'visitedByType.action.viewMore': 'View more'; readonly 'visitedByType.action.viewLess': 'View less'; readonly 'featuredDocsCard.empty.title': 'No documents to show'; diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 9db31259dd..59e13213f2 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -24,8 +24,8 @@ * @packageDocumentation */ +import { lazy as reactLazy } from 'react'; import { - coreExtensionData, createExtensionInput, PageBlueprint, NavItemBlueprint, @@ -36,37 +36,60 @@ import { storageApiRef, errorApiRef, ApiBlueprint, + ExtensionBoundary, } from '@backstage/frontend-plugin-api'; import { VisitListener } from './components/'; import { visitsApiRef, VisitsStorageApi, VisitsWebStorageApi } from './api'; import HomeIcon from '@material-ui/icons/Home'; +import { + homePageWidgetDataRef, + homePageLayoutComponentDataRef, + HomePageLayoutBlueprint, + type HomePageLayoutProps, +} from '@backstage/plugin-home-react/alpha'; const rootRouteRef = createRouteRef(); const homePage = PageBlueprint.makeWithOverrides({ inputs: { - props: createExtensionInput( - [ - coreExtensionData.reactElement.optional(), - coreExtensionData.title.optional(), - ], - { - singleton: true, - optional: true, - }, - ), + widgets: createExtensionInput([homePageWidgetDataRef]), + layouts: createExtensionInput([HomePageLayoutBlueprint.dataRefs.component]), }, - factory: (originalFactory, { inputs }) => { + factory(originalFactory, { node, inputs }) { return originalFactory({ path: '/home', routeRef: rootRouteRef, - loader: () => - import('./components/').then(m => ( - - )), + loader: async () => { + const LazyDefaultLayout = reactLazy(() => + import('./alpha/DefaultHomePageLayout').then(m => ({ + default: m.DefaultHomePageLayout, + })), + ); + + const DefaultLayoutComponent = (props: HomePageLayoutProps) => ( + + + + ); + + const layouts = [ + ...inputs.layouts.map(layout => ({ + Component: layout.get(homePageLayoutComponentDataRef), + })), + { + Component: DefaultLayoutComponent, + }, + ]; + + const widgets = inputs.widgets.map(widget => + widget.get(homePageWidgetDataRef), + ); + + // Use the first installed layout, or fall back to the default + const layout = layouts[0]; + + return ; + }, }); }, }); @@ -126,12 +149,6 @@ export default createFrontendPlugin({ }); export { homeTranslationRef } from './translation'; -export { - HomepageBlueprint, - type HomepageBlueprintParams, - type HomepageTemplateProps, - type HomepageGridProps, -} from './alpha/HomepageBlueprint'; export { type LayoutConfiguration, type Breakpoint, diff --git a/plugins/home/src/alpha/DefaultHomePageLayout.tsx b/plugins/home/src/alpha/DefaultHomePageLayout.tsx new file mode 100644 index 0000000000..f8d63a635f --- /dev/null +++ b/plugins/home/src/alpha/DefaultHomePageLayout.tsx @@ -0,0 +1,39 @@ +/* + * 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 { Fragment } from 'react'; +import { Content } from '@backstage/core-components'; +import type { HomePageLayoutProps } from '@backstage/plugin-home-react/alpha'; +import { CustomHomepageGrid } from '../components'; + +/** + * The default home page layout component. + * + * Renders widgets inside a `` wrapped in ``. + * This is used when no custom layout extension is installed. + */ +export function DefaultHomePageLayout(props: HomePageLayoutProps) { + const { widgets } = props; + return ( + + + {widgets.map((widget, index) => ( + {widget.component} + ))} + + + ); +} diff --git a/plugins/home/src/alpha/HomepageBlueprint.tsx b/plugins/home/src/alpha/HomepageBlueprint.tsx deleted file mode 100644 index b72ff4d587..0000000000 --- a/plugins/home/src/alpha/HomepageBlueprint.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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 { compatWrapper } from '@backstage/core-compat-api'; -import { Content } from '@backstage/core-components'; -import { - coreExtensionData, - createExtensionBlueprint, - createExtensionInput, - ExtensionBoundary, -} from '@backstage/frontend-plugin-api'; -import { Fragment, type ReactElement, type ReactNode } from 'react'; -import { CustomHomepageGrid } from '../components'; -import type { CustomHomepageGridProps } from '../components'; -import { homePageWidgetDataRef } from '@backstage/plugin-home-react/alpha'; - -/** - * Arguments provided to the homepage renderer. - * - * @alpha - */ -export interface HomepageTemplateProps { - /** - * React elements built from the installed homepage widgets. - */ - widgets: ReactNode[]; - /** - * A element that renders the widgets using the provided props. - */ - grid: ReactElement; -} - -/** - * Parameters for creating a homepage extension. - * - * @alpha - */ -export interface HomepageBlueprintParams { - /** - * Optional title used by the home page when rendered through the new frontend system. - */ - title?: string; - /** - * Props forwarded to . The `children` prop is managed by the blueprint. - */ - grid?: Omit; - /** - * Allows supplying a custom renderer for the homepage. Receives the generated widgets as well - * as a element configured with the provided props. - */ - render?: (props: HomepageTemplateProps) => ReactElement; -} - -const DEFAULT_ATTACH_POINT = Object.freeze({ - id: 'page:home', - input: 'props', -}); - -/** - * Blueprint that composes a home page based on installed widgets. - * - * @alpha - */ -export const HomepageBlueprint = createExtensionBlueprint({ - kind: 'home-page', - attachTo: DEFAULT_ATTACH_POINT, - output: [coreExtensionData.reactElement, coreExtensionData.title.optional()], - inputs: { - widgets: createExtensionInput([homePageWidgetDataRef]), - }, - *factory(params: HomepageBlueprintParams = {}, { inputs, node }) { - const widgetOutputs = inputs.widgets ?? []; - const widgetElements = widgetOutputs.map((widget, index) => ( - - {widget.get(homePageWidgetDataRef).component} - - )); - - const gridElement = ( - - {widgetElements} - - ); - - const renderedElement = params.render?.({ - widgets: widgetElements, - grid: gridElement, - }) ?? {gridElement}; - - yield coreExtensionData.reactElement( - - {compatWrapper(renderedElement)} - , - ); - - if (params.title) { - yield coreExtensionData.title(params.title); - } - }, -}); - -export type { CustomHomepageGridProps as HomepageGridProps } from '../components'; From 1ba0ee341a6fe5e095320f2f1b82acba5909e5fe Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sat, 7 Feb 2026 05:02:22 -0800 Subject: [PATCH 08/12] docs(home): update docs and app-next example for HomePageLayoutBlueprint - Update README.md to reference HomePageLayoutBlueprint instead of deleted HomepageBlueprint, with a complete working example - Update docs/getting-started/homepage.md with corrected blueprint name - Fix app-next example to properly accept and render widgets via the HomePageLayoutProps interface instead of ignoring them Signed-off-by: Adam Kunicki --- docs/getting-started/homepage.md | 2 +- packages/app-next/src/App.tsx | 42 ++++++++++++++++++++++++----- plugins/home/README.md | 46 +++++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 8faedd67eb..aac8408797 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -92,7 +92,7 @@ app: The New Frontend System provides powerful customization options: -**Custom Homepage Layouts**: Use the `HomepageBlueprint` to create custom homepage layouts with your own design and widget arrangements. +**Custom Homepage Layouts**: Use the `HomePageLayoutBlueprint` from `@backstage/plugin-home-react/alpha` to create custom homepage layouts with your own design and widget arrangements. A layout receives the installed widgets and is responsible for rendering them. If no custom layout is installed, the plugin provides a built-in default. **Adding Homepage Widgets**: Register custom widgets using the `HomepageWidgetBlueprint` from the `@backstage/plugin-home-react/alpha` package. diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 2674493b8e..88de62663b 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -21,7 +21,18 @@ import userSettingsPlugin from '@backstage/plugin-user-settings/alpha'; import homePlugin from '@backstage/plugin-home/alpha'; import { createFrontendModule } from '@backstage/frontend-plugin-api'; -import { HomePageLayoutBlueprint } from '@backstage/plugin-home-react/alpha'; +import { + HomePageLayoutBlueprint, + type HomePageLayoutProps, +} from '@backstage/plugin-home-react/alpha'; +import { Fragment } from 'react'; +import { Content, Header, Page } from '@backstage/core-components'; +import { + CustomHomepageGrid, + WelcomeTitle, + HeaderWorldClock, + type ClockConfig, +} from '@backstage/plugin-home'; import { techdocsPlugin, TechDocsIndexPage, @@ -29,7 +40,6 @@ import { EntityTechdocsContent, } from '@backstage/plugin-techdocs'; import appVisualizerPlugin from '@backstage/plugin-app-visualizer'; -import { homePage } from './HomePage'; import { convertLegacyAppRoot } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; import { Route } from 'react-router'; @@ -93,16 +103,36 @@ const convertedTechdocsPlugin = convertLegacyPlugin(techdocsPlugin, { ], }); +const clockConfigs: ClockConfig[] = [ + { label: 'NYC', timeZone: 'America/New_York' }, + { label: 'UTC', timeZone: 'UTC' }, + { label: 'STO', timeZone: 'Europe/Stockholm' }, + { label: 'TYO', timeZone: 'Asia/Tokyo' }, +]; + const customHomePageModule = createFrontendModule({ pluginId: 'home', extensions: [ HomePageLayoutBlueprint.make({ params: { loader: async () => - // This is a legacy-style home page that renders static content. - // In production, you'd use the widgets prop to render dynamic widgets. - function LegacyHomePageLayout() { - return homePage; + function CustomHomePageLayout({ widgets }: HomePageLayoutProps) { + return ( + +
} pageTitleOverride="Home"> + +
+ + + {widgets.map((widget, index) => ( + + {widget.component} + + ))} + + +
+ ); }, }, }), diff --git a/plugins/home/README.md b/plugins/home/README.md index 10bebe6195..fe16c3f221 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -41,25 +41,51 @@ The plugin will automatically provide: #### Creating Custom Homepage Layouts -Use the `HomepageBlueprint` to create custom homepage layouts: +Use the `HomePageLayoutBlueprint` from `@backstage/plugin-home-react/alpha` to +create custom homepage layouts. A layout receives the installed widgets and is +responsible for arranging them on the page. If no custom layout is installed, the +plugin provides a built-in default. ```ts -import { HomepageBlueprint } from '@backstage/plugin-home/alpha'; +import { HomePageLayoutBlueprint } from '@backstage/plugin-home-react/alpha'; +import { CustomHomepageGrid } from '@backstage/plugin-home'; import { Content, Header, Page } from '@backstage/core-components'; +import { Fragment } from 'react'; -const myHomePage = HomepageBlueprint.make({ +const myHomePageLayout = HomePageLayoutBlueprint.make({ params: { - title: 'My Custom Home', - render: ({ grid }) => ( - -
- {grid} - - ), + loader: async () => + function MyHomePageLayout({ widgets }) { + return ( + +
+ + + {widgets.map((widget, index) => ( + + {widget.component} + + ))} + + + + ); + }, }, }); ``` +Then register the layout in a frontend module: + +```ts +import { createFrontendModule } from '@backstage/frontend-plugin-api'; + +const homeModule = createFrontendModule({ + pluginId: 'home', + extensions: [myHomePageLayout], +}); +``` + #### Visit Tracking (Optional) Visit tracking is an **optional feature** that must be explicitly enabled. When enabled, it provides intelligent storage fallbacks: From e4544fb7732695a178dd2ba86fc7e562a0ef0667 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sat, 7 Feb 2026 05:11:35 -0800 Subject: [PATCH 09/12] refactor(home): rename HomepageWidgetBlueprint to HomePageWidgetBlueprint Align naming convention with HomePageLayoutBlueprint and the catalog plugin's consistent entity- prefix pattern. Also update the kind string from 'home-widget' to 'home-page-widget' to match 'home-page-layout'. Added interface-level JSDoc to HomePageWidgetBlueprintParams. Signed-off-by: Adam Kunicki --- docs/getting-started/homepage.md | 2 +- plugins/home-react/report-alpha.api.md | 10 +++++----- plugins/home-react/src/alpha.ts | 6 +++--- ...etBlueprint.tsx => HomePageWidgetBlueprint.tsx} | 14 +++++++++----- plugins/home/README.md | 8 ++++---- plugins/home/dev/index.tsx | 8 ++++---- 6 files changed, 26 insertions(+), 22 deletions(-) rename plugins/home-react/src/alpha/blueprints/{HomepageWidgetBlueprint.tsx => HomePageWidgetBlueprint.tsx} (91%) diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index aac8408797..65b866414b 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -94,7 +94,7 @@ The New Frontend System provides powerful customization options: **Custom Homepage Layouts**: Use the `HomePageLayoutBlueprint` from `@backstage/plugin-home-react/alpha` to create custom homepage layouts with your own design and widget arrangements. A layout receives the installed widgets and is responsible for rendering them. If no custom layout is installed, the plugin provides a built-in default. -**Adding Homepage Widgets**: Register custom widgets using the `HomepageWidgetBlueprint` from the `@backstage/plugin-home-react/alpha` package. +**Adding Homepage Widgets**: Register custom widgets using the `HomePageWidgetBlueprint` from the `@backstage/plugin-home-react/alpha` package. For detailed instructions on creating custom layouts, registering widgets, and advanced configuration options, see the [Home plugin documentation](https://github.com/backstage/backstage/tree/master/plugins/home#readme). diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index 0fc6360e35..cd672719b1 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -79,9 +79,9 @@ export interface HomePageLayoutProps { } // @alpha -export const HomepageWidgetBlueprint: ExtensionBlueprint<{ - kind: 'home-widget'; - params: HomepageWidgetBlueprintParams; +export const HomePageWidgetBlueprint: ExtensionBlueprint<{ + kind: 'home-page-widget'; + params: HomePageWidgetBlueprintParams; output: ExtensionDataRef; inputs: {}; config: {}; @@ -95,8 +95,8 @@ export const HomepageWidgetBlueprint: ExtensionBlueprint<{ }; }>; -// @alpha (undocumented) -export interface HomepageWidgetBlueprintParams { +// @alpha +export interface HomePageWidgetBlueprintParams { componentProps?: Record; components: () => Promise; description?: string; diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 1a3432e02e..00b7a7bed7 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -25,9 +25,9 @@ */ export { homeReactTranslationRef } from './translation'; export { - HomepageWidgetBlueprint, - type HomepageWidgetBlueprintParams, -} from './alpha/blueprints/HomepageWidgetBlueprint'; + HomePageWidgetBlueprint, + type HomePageWidgetBlueprintParams, +} from './alpha/blueprints/HomePageWidgetBlueprint'; export { HomePageLayoutBlueprint, type HomePageLayoutBlueprintParams, diff --git a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx similarity index 91% rename from plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx rename to plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx index e3375f2cb9..1dd1dc5501 100644 --- a/plugins/home-react/src/alpha/blueprints/HomepageWidgetBlueprint.tsx +++ b/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx @@ -28,8 +28,12 @@ import { } from '../../extensions'; import { homePageWidgetDataRef } from '../dataRefs'; -/** @alpha */ -export interface HomepageWidgetBlueprintParams { +/** + * Parameters for creating a home page widget extension. + * + * @alpha + */ +export interface HomePageWidgetBlueprintParams { /** * Optional name for the widget. If not provided, the extension will use only its kind * in the extension ID. @@ -71,14 +75,14 @@ const DEFAULT_WIDGET_ATTACH_POINT = { * * @alpha */ -export const HomepageWidgetBlueprint = createExtensionBlueprint({ - kind: 'home-widget', +export const HomePageWidgetBlueprint = createExtensionBlueprint({ + kind: 'home-page-widget', attachTo: DEFAULT_WIDGET_ATTACH_POINT, dataRefs: { widget: homePageWidgetDataRef, }, output: [homePageWidgetDataRef], - *factory(params: HomepageWidgetBlueprintParams, { node }) { + *factory(params: HomePageWidgetBlueprintParams, { node }) { const isCustomizable = params.settings?.schema !== undefined; const LazyCard = lazy(() => params.components().then(parts => ({ diff --git a/plugins/home/README.md b/plugins/home/README.md index fe16c3f221..e2da69a32b 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -114,17 +114,17 @@ app: Homepage widgets are React components that can be added to customizable home pages. The **key difference** between the new frontend system and legacy system is how these widget components are **registered and exported**: -- **New Frontend System**: Use `HomepageWidgetBlueprint` to register widgets as extensions +- **New Frontend System**: Use `HomePageWidgetBlueprint` to register widgets as extensions - **Legacy System**: Use `createCardExtension` to export widgets as card components ### New Frontend System -Create widgets using the `HomepageWidgetBlueprint`: +Create widgets using the `HomePageWidgetBlueprint`: ```ts -import { HomepageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; +import { HomePageWidgetBlueprint } from '@backstage/plugin-home-react/alpha'; -const myWidget = HomepageWidgetBlueprint.make({ +const myWidget = HomePageWidgetBlueprint.make({ name: 'my-widget', params: { name: 'MyWidget', diff --git a/plugins/home/dev/index.tsx b/plugins/home/dev/index.tsx index ce292c4ee5..954bf21740 100644 --- a/plugins/home/dev/index.tsx +++ b/plugins/home/dev/index.tsx @@ -28,7 +28,7 @@ import { createFrontendModule, } from '@backstage/frontend-plugin-api'; import { - HomepageWidgetBlueprint, + HomePageWidgetBlueprint, HomePageLayoutBlueprint, } from '@backstage/plugin-home-react/alpha'; import { HeaderWorldClock, WelcomeTitle, type ClockConfig } from '../src'; @@ -102,7 +102,7 @@ const homePageLayout = HomePageLayoutBlueprint.make({ }, }); -const homePageToolkitWidget = HomepageWidgetBlueprint.make({ +const homePageToolkitWidget = HomePageWidgetBlueprint.make({ name: 'home-toolkit', params: { name: 'HomePageToolkit', @@ -124,7 +124,7 @@ const homePageToolkitWidget = HomepageWidgetBlueprint.make({ }, }); -const homePageStarredEntitiesWidget = HomepageWidgetBlueprint.make({ +const homePageStarredEntitiesWidget = HomePageWidgetBlueprint.make({ name: 'home-starred-entities', params: { name: 'HomePageStarredEntities', @@ -136,7 +136,7 @@ const homePageStarredEntitiesWidget = HomepageWidgetBlueprint.make({ }, }); -const homePageRandomJokeWidget = HomepageWidgetBlueprint.make({ +const homePageRandomJokeWidget = HomePageWidgetBlueprint.make({ name: 'home-random-joke', params: { name: 'HomePageRandomJoke', From 56c58877b13aa7a75124bfe8281a06348d0d5158 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sun, 8 Feb 2026 19:42:11 -0800 Subject: [PATCH 10/12] refactor(home): align layout input and widget data wiring Signed-off-by: Adam Kunicki --- plugins/home-react/report-alpha.api.md | 2 ++ .../blueprints/HomePageLayoutBlueprint.tsx | 2 +- .../blueprints/HomePageWidgetBlueprint.tsx | 1 + plugins/home-react/src/alpha/dataRefs.ts | 9 ++++- plugins/home/report-alpha.api.md | 24 +++----------- plugins/home/src/alpha.tsx | 33 ++++++++----------- 6 files changed, 29 insertions(+), 42 deletions(-) diff --git a/plugins/home-react/report-alpha.api.md b/plugins/home-react/report-alpha.api.md index cd672719b1..0de77172a0 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AppNode } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; @@ -112,6 +113,7 @@ export interface HomePageWidgetData { description?: string; layout?: CardLayout; name?: string; + node: AppNode; settings?: CardSettings; title?: string; } diff --git a/plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx index c1d4e94ffb..9610b79993 100644 --- a/plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx +++ b/plugins/home-react/src/alpha/blueprints/HomePageLayoutBlueprint.tsx @@ -51,7 +51,7 @@ export interface HomePageLayoutBlueprintParams { */ export const HomePageLayoutBlueprint = createExtensionBlueprint({ kind: 'home-page-layout', - attachTo: { id: 'page:home', input: 'layouts' }, + attachTo: { id: 'page:home', input: 'layout' }, output: [homePageLayoutComponentDataRef], dataRefs: { component: homePageLayoutComponentDataRef, diff --git a/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx index 1dd1dc5501..755713a363 100644 --- a/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx +++ b/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx @@ -106,6 +106,7 @@ export const HomePageWidgetBlueprint = createExtensionBlueprint({ ); yield homePageWidgetDataRef({ + node, component: , name: params.name, title: params.title, diff --git a/plugins/home-react/src/alpha/dataRefs.ts b/plugins/home-react/src/alpha/dataRefs.ts index 2fc6ee30fa..b626289541 100644 --- a/plugins/home-react/src/alpha/dataRefs.ts +++ b/plugins/home-react/src/alpha/dataRefs.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { createExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { + createExtensionDataRef, + type AppNode, +} from '@backstage/frontend-plugin-api'; import { JSX, ReactElement } from 'react'; import type { CardLayout, CardSettings } from '../extensions'; @@ -25,6 +28,10 @@ import type { CardLayout, CardSettings } from '../extensions'; * @alpha */ export interface HomePageWidgetData { + /** + * The originating app node for this widget. + */ + node: AppNode; /** * The rendered widget component (typically a card with header, content, etc.) */ diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 5214736c1c..fb7e6bb53d 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -16,13 +16,9 @@ import { IconComponent } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; -import { ReactElement } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @public -export type Breakpoint = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; - // @alpha const _default: OverridableFrontendPlugin< { @@ -107,16 +103,16 @@ const _default: OverridableFrontendPlugin< internal: false; } >; - layouts: ExtensionInput< + layout: ExtensionInput< ConfigurableExtensionDataRef< (props: HomePageLayoutProps) => JSX_2.Element, 'home.layout.component', {} >, { - singleton: false; - optional: false; - internal: false; + singleton: true; + optional: true; + internal: true; } >; }; @@ -166,17 +162,5 @@ export const homeTranslationRef: TranslationRef< } >; -// @public -export type LayoutConfiguration = { - component: ReactElement | string; - x: number; - y: number; - width: number; - height: number; - movable?: boolean; - deletable?: boolean; - resizable?: boolean; -}; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index 59e13213f2..fea06f914e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -53,7 +53,11 @@ const rootRouteRef = createRouteRef(); const homePage = PageBlueprint.makeWithOverrides({ inputs: { widgets: createExtensionInput([homePageWidgetDataRef]), - layouts: createExtensionInput([HomePageLayoutBlueprint.dataRefs.component]), + layout: createExtensionInput([HomePageLayoutBlueprint.dataRefs.component], { + singleton: true, + optional: true, + internal: true, + }), }, factory(originalFactory, { node, inputs }) { return originalFactory({ @@ -72,23 +76,16 @@ const homePage = PageBlueprint.makeWithOverrides({ ); - const layouts = [ - ...inputs.layouts.map(layout => ({ - Component: layout.get(homePageLayoutComponentDataRef), - })), - { - Component: DefaultLayoutComponent, - }, - ]; + const Layout = + inputs.layout?.get(homePageLayoutComponentDataRef) ?? + DefaultLayoutComponent; - const widgets = inputs.widgets.map(widget => - widget.get(homePageWidgetDataRef), - ); + const widgets = inputs.widgets.map(widget => ({ + ...widget.get(homePageWidgetDataRef), + node: widget.node, + })); - // Use the first installed layout, or fall back to the default - const layout = layouts[0]; - - return ; + return ; }, }); }, @@ -149,7 +146,3 @@ export default createFrontendPlugin({ }); export { homeTranslationRef } from './translation'; -export { - type LayoutConfiguration, - type Breakpoint, -} from './components/CustomHomepage/types'; From d7b8ff09f56dcef5b1ee2b9db5a4b8303b492825 Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sun, 8 Feb 2026 21:51:50 -0800 Subject: [PATCH 11/12] feat(home): register default alpha homepage widgets Signed-off-by: Adam Kunicki --- plugins/home/report-alpha.api.md | 40 +++++++++++++--- plugins/home/src/alpha.tsx | 79 +++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index fb7e6bb53d..83f1bb15c9 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -11,6 +11,7 @@ import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionInput } from '@backstage/frontend-plugin-api'; import { HomePageLayoutProps } from '@backstage/plugin-home-react/alpha'; +import { HomePageWidgetBlueprintParams } from '@backstage/plugin-home-react/alpha'; import { HomePageWidgetData } from '@backstage/plugin-home-react/alpha'; import { IconComponent } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; @@ -52,6 +53,33 @@ const _default: OverridableFrontendPlugin< element: JSX.Element; }; }>; + 'home-page-widget:home/random-joke': OverridableExtensionDefinition<{ + kind: 'home-page-widget'; + name: 'random-joke'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: HomePageWidgetBlueprintParams; + }>; + 'home-page-widget:home/starred-entities': OverridableExtensionDefinition<{ + kind: 'home-page-widget'; + name: 'starred-entities'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: HomePageWidgetBlueprintParams; + }>; + 'home-page-widget:home/toolkit': OverridableExtensionDefinition<{ + kind: 'home-page-widget'; + name: 'toolkit'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: HomePageWidgetBlueprintParams; + }>; 'nav-item:home': OverridableExtensionDefinition<{ kind: 'nav-item'; name: undefined; @@ -133,6 +161,7 @@ export default _default; export const homeTranslationRef: TranslationRef< 'home', { + readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!'; readonly 'addWidgetDialog.title': 'Add new widget to dashboard'; readonly 'customHomepageButtons.cancel': 'Cancel'; readonly 'customHomepageButtons.clearAll': 'Clear all'; @@ -141,22 +170,21 @@ export const homeTranslationRef: TranslationRef< readonly 'customHomepageButtons.addWidget': 'Add widget'; readonly 'customHomepageButtons.save': 'Save'; readonly 'customHomepage.noWidgets': "No widgets added. Start by clicking the 'Add widget' button."; + readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel'; readonly 'widgetSettingsOverlay.editSettingsTooptip': 'Edit settings'; readonly 'widgetSettingsOverlay.deleteWidgetTooltip': 'Delete widget'; readonly 'widgetSettingsOverlay.submitButtonTitle': 'Submit'; - readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel'; readonly 'starredEntityListItem.removeFavoriteEntityTitle': 'Remove entity from favorites'; - readonly 'visitList.empty.title': 'There are no visits to show yet.'; - readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.'; readonly 'visitList.few.title': 'The more pages you visit, the more pages will appear here.'; - readonly 'quickStart.title': 'Onboarding'; + readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.'; + readonly 'visitList.empty.title': 'There are no visits to show yet.'; readonly 'quickStart.description': 'Get started with Backstage'; + readonly 'quickStart.title': 'Onboarding'; readonly 'quickStart.learnMoreLinkTitle': 'Learn more'; - readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!'; readonly 'visitedByType.action.viewMore': 'View more'; readonly 'visitedByType.action.viewLess': 'View less'; - readonly 'featuredDocsCard.empty.title': 'No documents to show'; readonly 'featuredDocsCard.empty.description': 'Create your own document. Check out our Getting Started Information'; + readonly 'featuredDocsCard.empty.title': 'No documents to show'; readonly 'featuredDocsCard.empty.learnMoreLinkTitle': 'DOCS'; readonly 'featuredDocsCard.learnMoreTitle': 'LEARN MORE'; } diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index fea06f914e..23e9d0e42b 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -45,6 +45,7 @@ import { homePageWidgetDataRef, homePageLayoutComponentDataRef, HomePageLayoutBlueprint, + HomePageWidgetBlueprint, type HomePageLayoutProps, } from '@backstage/plugin-home-react/alpha'; @@ -128,6 +129,74 @@ const homeNavItem = NavItemBlueprint.make({ }, }); +const homePageToolkitWidget = HomePageWidgetBlueprint.make({ + name: 'toolkit', + params: { + name: 'HomePageToolkit', + title: 'Toolkit', + components: () => + import('./homePageComponents/Toolkit').then(m => ({ + Content: m.Content, + ContextProvider: m.ContextProvider, + })), + componentProps: { + tools: [ + { + url: 'https://backstage.io', + label: 'Backstage Docs', + icon: , + }, + ], + }, + }, +}); + +const homePageStarredEntitiesWidget = HomePageWidgetBlueprint.make({ + name: 'starred-entities', + params: { + name: 'HomePageStarredEntities', + title: 'Your Starred Entities', + components: () => + import('./homePageComponents/StarredEntities').then(m => ({ + Content: m.Content, + })), + }, +}); + +const homePageRandomJokeWidget = HomePageWidgetBlueprint.make({ + name: 'random-joke', + params: { + name: 'HomePageRandomJoke', + title: 'Random Joke', + description: 'Shows a random programming joke', + components: () => + import('./homePageComponents/RandomJoke').then(m => ({ + Content: m.Content, + Settings: m.Settings, + Actions: m.Actions, + ContextProvider: m.ContextProvider, + })), + layout: { + height: { minRows: 4 }, + width: { minColumns: 3 }, + }, + settings: { + schema: { + title: 'Random Joke settings', + type: 'object', + properties: { + defaultCategory: { + title: 'Category', + type: 'string', + enum: ['any', 'programming', 'dad'], + default: 'any', + }, + }, + }, + }, + }, +}); + /** * Home plugin for the new frontend system. * @@ -139,7 +208,15 @@ const homeNavItem = NavItemBlueprint.make({ export default createFrontendPlugin({ pluginId: 'home', info: { packageJson: () => import('../package.json') }, - extensions: [homePage, homeNavItem, visitsApi, visitListenerAppRootElement], + extensions: [ + homePage, + homeNavItem, + visitsApi, + visitListenerAppRootElement, + homePageToolkitWidget, + homePageStarredEntitiesWidget, + homePageRandomJokeWidget, + ], routes: { root: rootRouteRef, }, From bfa2bc55df3ed75246b7c0c9e445217162e3db2c Mon Sep 17 00:00:00 2001 From: Adam Kunicki Date: Sun, 8 Feb 2026 21:57:16 -0800 Subject: [PATCH 12/12] fix(home): expose alpha widget metadata to custom grid Signed-off-by: Adam Kunicki --- .../src/alpha/blueprints/HomePageWidgetBlueprint.tsx | 12 +++++++++++- plugins/home/report-alpha.api.md | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx b/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx index 755713a363..4cd38754fa 100644 --- a/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx +++ b/plugins/home-react/src/alpha/blueprints/HomePageWidgetBlueprint.tsx @@ -19,6 +19,7 @@ import { createExtensionBlueprint, ExtensionBoundary, } from '@backstage/frontend-plugin-api'; +import { attachComponentData } from '@backstage/core-plugin-api'; import { CardExtension, CardExtensionProps, @@ -84,6 +85,7 @@ export const HomePageWidgetBlueprint = createExtensionBlueprint({ output: [homePageWidgetDataRef], *factory(params: HomePageWidgetBlueprintParams, { node }) { const isCustomizable = params.settings?.schema !== undefined; + const widgetName = params.name ?? node.spec.id; const LazyCard = lazy(() => params.components().then(parts => ({ default: (props: CardExtensionProps>) => ( @@ -105,10 +107,18 @@ export const HomePageWidgetBlueprint = createExtensionBlueprint({ ); + attachComponentData(Widget, 'core.extensionName', widgetName); + attachComponentData(Widget, 'title', params.title); + attachComponentData(Widget, 'description', params.description); + attachComponentData(Widget, 'home.widget.config', { + layout: params.layout, + settings: params.settings, + }); + yield homePageWidgetDataRef({ node, component: , - name: params.name, + name: widgetName, title: params.title, description: params.description, layout: params.layout, diff --git a/plugins/home/report-alpha.api.md b/plugins/home/report-alpha.api.md index 83f1bb15c9..4568fc8c2a 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -175,16 +175,16 @@ export const homeTranslationRef: TranslationRef< readonly 'widgetSettingsOverlay.deleteWidgetTooltip': 'Delete widget'; readonly 'widgetSettingsOverlay.submitButtonTitle': 'Submit'; readonly 'starredEntityListItem.removeFavoriteEntityTitle': 'Remove entity from favorites'; - readonly 'visitList.few.title': 'The more pages you visit, the more pages will appear here.'; - readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.'; readonly 'visitList.empty.title': 'There are no visits to show yet.'; - readonly 'quickStart.description': 'Get started with Backstage'; + readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.'; + readonly 'visitList.few.title': 'The more pages you visit, the more pages will appear here.'; readonly 'quickStart.title': 'Onboarding'; + readonly 'quickStart.description': 'Get started with Backstage'; readonly 'quickStart.learnMoreLinkTitle': 'Learn more'; readonly 'visitedByType.action.viewMore': 'View more'; readonly 'visitedByType.action.viewLess': 'View less'; - readonly 'featuredDocsCard.empty.description': 'Create your own document. Check out our Getting Started Information'; readonly 'featuredDocsCard.empty.title': 'No documents to show'; + readonly 'featuredDocsCard.empty.description': 'Create your own document. Check out our Getting Started Information'; readonly 'featuredDocsCard.empty.learnMoreLinkTitle': 'DOCS'; readonly 'featuredDocsCard.learnMoreTitle': 'LEARN MORE'; }