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..65b866414b 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 `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. + +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..88de62663b 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -18,15 +18,21 @@ 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 { createFrontendModule } from '@backstage/frontend-plugin-api'; import { - coreExtensionData, - createExtension, - createFrontendModule, -} from '@backstage/frontend-plugin-api'; + 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, @@ -34,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'; @@ -98,18 +103,37 @@ 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: [ - createExtension({ - name: 'my-home-page', - attachTo: { id: 'page:home', input: 'props' }, - output: [coreExtensionData.reactElement, titleExtensionDataRef], - factory() { - return [ - coreExtensionData.reactElement(homePage), - titleExtensionDataRef('just a title'), - ]; + HomePageLayoutBlueprint.make({ + params: { + loader: async () => + function CustomHomePageLayout({ widgets }: HomePageLayoutProps) { + return ( + +
} pageTitleOverride="Home"> + +
+ + + {widgets.map((widget, index) => ( + + {widget.component} + + ))} + + +
+ ); + }, }, }), ], 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..0de77172a0 100644 --- a/plugins/home-react/report-alpha.api.md +++ b/plugins/home-react/report-alpha.api.md @@ -3,9 +3,129 @@ > 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'; +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'; -// @alpha (undocumented) +// @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 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-page-widget'; + params: HomePageWidgetBlueprintParams; + output: ExtensionDataRef; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + widget: ConfigurableExtensionDataRef< + HomePageWidgetData, + 'home.widget.data', + {} + >; + }; +}>; + +// @alpha +export interface HomePageWidgetBlueprintParams { + componentProps?: Record; + components: () => Promise; + description?: string; + layout?: CardLayout; + name?: string; + settings?: CardSettings; + title?: string; +} + +// @alpha +export interface HomePageWidgetData { + component: ReactElement; + description?: string; + layout?: CardLayout; + name?: string; + node: AppNode; + settings?: CardSettings; + title?: string; +} + +// @alpha +export const homePageWidgetDataRef: ConfigurableExtensionDataRef< + HomePageWidgetData, + 'home.widget.data', + {} +>; + +// @alpha export const homeReactTranslationRef: TranslationRef< 'home-react', { @@ -14,6 +134,4 @@ export const homeReactTranslationRef: TranslationRef< readonly 'cardExtension.settingsButtonTitle': 'Settings'; } >; - -// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home-react/src/alpha.ts b/plugins/home-react/src/alpha.ts index 3a08699f66..00b7a7bed7 100644 --- a/plugins/home-react/src/alpha.ts +++ b/plugins/home-react/src/alpha.ts @@ -13,4 +13,29 @@ * 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 { + 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..9610b79993 --- /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: 'layout' }, + 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 new file mode 100644 index 0000000000..4cd38754fa --- /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 { + createExtensionBlueprint, + ExtensionBoundary, +} from '@backstage/frontend-plugin-api'; +import { attachComponentData } from '@backstage/core-plugin-api'; +import { + CardExtension, + CardExtensionProps, + CardLayout, + CardSettings, + ComponentParts, +} from '../../extensions'; +import { homePageWidgetDataRef } from '../dataRefs'; + +/** + * 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. + */ + 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; + +/** + * Creates widgets that can be installed into the home page grid. + * + * @alpha + */ +export const HomePageWidgetBlueprint = createExtensionBlueprint({ + kind: 'home-page-widget', + attachTo: DEFAULT_WIDGET_ATTACH_POINT, + dataRefs: { + widget: homePageWidgetDataRef, + }, + 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>) => ( + + ), + })), + ); + + const Widget = ( + props: CardExtensionProps>, + ): ReactElement => ( + + + + ); + + 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: widgetName, + 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..b626289541 --- /dev/null +++ b/plugins/home-react/src/alpha/dataRefs.ts @@ -0,0 +1,100 @@ +/* + * 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, + type AppNode, +} from '@backstage/frontend-plugin-api'; +import { JSX, 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 originating app node for this widget. + */ + node: AppNode; + /** + * 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', + }); + +/** + * 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-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..e2da69a32b 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,170 @@ 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 `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 { 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 myHomePageLayout = HomePageLayoutBlueprint.make({ + params: { + 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: + +**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 +204,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 +442,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..954bf21740 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,215 @@ * 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 { Fragment } from 'react'; + +import { + ApiBlueprint, + createFrontendModule, +} from '@backstage/frontend-plugin-api'; +import { + HomePageWidgetBlueprint, + HomePageLayoutBlueprint, +} from '@backstage/plugin-home-react/alpha'; +import { HeaderWorldClock, WelcomeTitle, type ClockConfig } from '../src'; +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' }, + { 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: LayoutConfiguration[] = [ + { + 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 homePageLayout = HomePageLayoutBlueprint.make({ + params: { + loader: async () => + function CustomHomePageLayout({ widgets }) { + return ( + +
} pageTitleOverride="Home"> + +
+ + + {widgets.map((widget, index) => ( + + {widget.component} + + ))} + + +
+ ); + }, + }, +}); + +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: [ + homePageLayout, + 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..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:^", @@ -83,6 +84,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..4568fc8c2a 100644 --- a/plugins/home/report-alpha.api.md +++ b/plugins/home/report-alpha.api.md @@ -10,13 +10,17 @@ import { ConfigurableExtensionDataRef } 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 { 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'; import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/frontend-plugin-api'; -// @alpha (undocumented) +// @alpha const _default: OverridableFrontendPlugin< { root: RouteRef; @@ -49,6 +53,54 @@ 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; + 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; @@ -67,25 +119,28 @@ const _default: OverridableFrontendPlugin< } >; inputs: { - props: ExtensionInput< - | ConfigurableExtensionDataRef< - JSX_2.Element, - 'core.reactElement', - { - optional: true; - } - > - | ConfigurableExtensionDataRef< - string, - 'title', - { - optional: true; - } - >, + widgets: ExtensionInput< + ConfigurableExtensionDataRef< + HomePageWidgetData, + 'home.widget.data', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + layout: ExtensionInput< + ConfigurableExtensionDataRef< + (props: HomePageLayoutProps) => JSX_2.Element, + 'home.layout.component', + {} + >, { singleton: true; optional: true; - internal: false; + internal: true; } >; }; @@ -102,7 +157,7 @@ const _default: OverridableFrontendPlugin< >; export default _default; -// @alpha (undocumented) +// @alpha export const homeTranslationRef: TranslationRef< 'home', { @@ -135,12 +190,5 @@ export const homeTranslationRef: TranslationRef< } >; -// @alpha (undocumented) -export const titleExtensionDataRef: ConfigurableExtensionDataRef< - string, - 'title', - {} ->; - // (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..23e9d0e42b 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -14,60 +14,87 @@ * 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 { lazy as reactLazy } from 'react'; import { - coreExtensionData, - createExtensionDataRef, createExtensionInput, PageBlueprint, + NavItemBlueprint, createFrontendPlugin, createRouteRef, AppRootElementBlueprint, identityApiRef, storageApiRef, + errorApiRef, ApiBlueprint, + ExtensionBoundary, } from '@backstage/frontend-plugin-api'; import { VisitListener } from './components/'; -import { visitsApiRef, VisitsStorageApi } from './api'; +import { visitsApiRef, VisitsStorageApi, VisitsWebStorageApi } from './api'; +import HomeIcon from '@material-ui/icons/Home'; +import { + homePageWidgetDataRef, + homePageLayoutComponentDataRef, + HomePageLayoutBlueprint, + HomePageWidgetBlueprint, + type HomePageLayoutProps, +} from '@backstage/plugin-home-react/alpha'; const rootRouteRef = createRouteRef(); -/** - * @alpha - */ -export const titleExtensionDataRef = createExtensionDataRef().with({ - id: 'title', -}); - const homePage = PageBlueprint.makeWithOverrides({ inputs: { - props: createExtensionInput( - [ - coreExtensionData.reactElement.optional(), - titleExtensionDataRef.optional(), - ], - { - singleton: true, - optional: true, - }, - ), + widgets: createExtensionInput([homePageWidgetDataRef]), + layout: createExtensionInput([HomePageLayoutBlueprint.dataRefs.component], { + singleton: true, + optional: true, + internal: true, + }), }, - 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 Layout = + inputs.layout?.get(homePageLayoutComponentDataRef) ?? + DefaultLayoutComponent; + + const widgets = inputs.widgets.map(widget => ({ + ...widget.get(homePageWidgetDataRef), + node: widget.node, + })); + + return ; + }, }); }, }); const visitListenerAppRootElement = AppRootElementBlueprint.make({ name: 'visit-listener', + disabled: true, params: { element: , }, @@ -75,25 +102,121 @@ 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, + }, +}); + +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. + * + * 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, + homePageToolkitWidget, + homePageStarredEntitiesWidget, + homePageRandomJokeWidget, + ], routes: { root: rootRouteRef, }, 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/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/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/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 ceca19aa30..61cdfd6deb 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:^" @@ -5928,10 +5929,13 @@ __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:^" + "@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:^" @@ -30972,6 +30976,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:^"