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 <kunickiaj@gmail.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-home-react': patch
|
||||
'@backstage/plugin-home': patch
|
||||
---
|
||||
|
||||
Support new frontend system in the homepage plugin
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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<JSX_2.Element, 'core.reactElement', {}>
|
||||
| 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<string, unknown>;
|
||||
components: () => Promise<ComponentParts>;
|
||||
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',
|
||||
{}
|
||||
>;
|
||||
```
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<ComponentParts>;
|
||||
/**
|
||||
* 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<string, unknown>;
|
||||
}
|
||||
|
||||
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<Record<string, unknown>>) => (
|
||||
<CardExtension
|
||||
{...props}
|
||||
{...parts}
|
||||
title={props.title || params.title}
|
||||
isCustomizable={isCustomizable}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
|
||||
const Widget = (
|
||||
props: CardExtensionProps<Record<string, unknown>>,
|
||||
): ReactElement =>
|
||||
compatWrapper(
|
||||
<ExtensionBoundary node={node}>
|
||||
<LazyCard {...props} />
|
||||
</ExtensionBoundary>,
|
||||
);
|
||||
|
||||
yield coreExtensionData.reactElement(
|
||||
<Widget {...(params.componentProps ?? {})} />,
|
||||
);
|
||||
|
||||
yield widgetMetadataRef({
|
||||
name: params.name,
|
||||
title: params.title,
|
||||
description: params.description,
|
||||
layout: params.layout,
|
||||
settings: params.settings,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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<string>().with({
|
||||
id: 'title',
|
||||
});
|
||||
@@ -121,7 +121,7 @@ type CardExtensionComponentProps<T> = CardExtensionProps<T> &
|
||||
overrideTitle?: string;
|
||||
};
|
||||
|
||||
function CardExtension<T>(props: CardExtensionComponentProps<T>) {
|
||||
export function CardExtension<T>(props: CardExtensionComponentProps<T>) {
|
||||
const {
|
||||
Renderer,
|
||||
Content,
|
||||
|
||||
@@ -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({
|
||||
|
||||
+144
-21
@@ -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 }) => (
|
||||
<Page themeId="home">
|
||||
<Header title="Welcome" />
|
||||
<Content>{grid}</Content>
|
||||
</Page>
|
||||
),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### 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: {
|
||||
|
||||
+202
-11
@@ -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: <HomepageCompositionRoot />,
|
||||
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<HomepageGridProps['config']> = [
|
||||
{
|
||||
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 }) => (
|
||||
<Page themeId="home">
|
||||
<Header title={<WelcomeTitle />} pageTitleOverride="Home">
|
||||
<HeaderWorldClock
|
||||
clockConfigs={clockConfigs}
|
||||
customTimeFormat={timeFormat}
|
||||
/>
|
||||
</Header>
|
||||
<Content>{grid}</Content>
|
||||
</Page>
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
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: <HomeIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<undefined>;
|
||||
@@ -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<undefined>;
|
||||
},
|
||||
'core.nav-item.target',
|
||||
{}
|
||||
>;
|
||||
inputs: {};
|
||||
params: {
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
routeRef: RouteRef<undefined>;
|
||||
};
|
||||
}>;
|
||||
'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<JSX_2.Element, 'core.reactElement', {}>
|
||||
| ExtensionDataRef<
|
||||
string,
|
||||
'title',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>;
|
||||
inputs: {
|
||||
widgets: ExtensionInput<
|
||||
ConfigurableExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>,
|
||||
{
|
||||
singleton: false;
|
||||
optional: false;
|
||||
}
|
||||
>;
|
||||
};
|
||||
config: {};
|
||||
configInput: {};
|
||||
dataRefs: never;
|
||||
}>;
|
||||
|
||||
// @alpha
|
||||
export interface HomepageBlueprintParams {
|
||||
grid?: Omit<HomepageGridProps, 'children'>;
|
||||
render?: (props: HomepageTemplateProps) => ReactElement;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type HomepageGridProps = {
|
||||
children?: ReactNode;
|
||||
config?: LayoutConfiguration[];
|
||||
title?: string;
|
||||
rowHeight?: number;
|
||||
breakpoints?: Record<Breakpoint, number>;
|
||||
cols?: Record<Breakpoint, number>;
|
||||
containerPadding?: [number, number] | Record<Breakpoint, [number, number]>;
|
||||
containerMargin?: [number, number] | Record<Breakpoint, [number, number]>;
|
||||
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)
|
||||
```
|
||||
|
||||
@@ -344,7 +344,7 @@ export type VisitsApiQueryParams = {
|
||||
}>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
// @public
|
||||
export const visitsApiRef: ApiRef<VisitsApi>;
|
||||
|
||||
// @public
|
||||
|
||||
@@ -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<string, string>();
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+49
-12
@@ -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<string>().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: <VisitListener />,
|
||||
},
|
||||
@@ -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';
|
||||
|
||||
@@ -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 <CustomHomepageGrid/> 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 <CustomHomepageGrid/>. The `children` prop is managed by the blueprint.
|
||||
*/
|
||||
grid?: Omit<CustomHomepageGridProps, 'children'>;
|
||||
/**
|
||||
* Allows supplying a custom renderer for the homepage. Receives the generated widgets as well
|
||||
* as a <CustomHomepageGrid/> 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) => (
|
||||
<Fragment key={index}>
|
||||
{widget.get(coreExtensionData.reactElement)}
|
||||
</Fragment>
|
||||
));
|
||||
|
||||
const gridElement = (
|
||||
<CustomHomepageGrid {...(params.grid ?? {})}>
|
||||
{widgetElements}
|
||||
</CustomHomepageGrid>
|
||||
);
|
||||
|
||||
const renderedElement = params.render?.({
|
||||
widgets: widgetElements,
|
||||
grid: gridElement,
|
||||
}) ?? <Content>{gridElement}</Content>;
|
||||
|
||||
yield coreExtensionData.reactElement(
|
||||
<ExtensionBoundary node={node}>
|
||||
{compatWrapper(renderedElement)}
|
||||
</ExtensionBoundary>,
|
||||
);
|
||||
|
||||
if (params.title) {
|
||||
yield titleExtensionDataRef(params.title);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export type { CustomHomepageGridProps as HomepageGridProps } from '../components';
|
||||
@@ -146,7 +146,12 @@ export interface VisitsApi {
|
||||
): Promise<Record<string, any>> | Record<string, any>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
/**
|
||||
* API reference for the visits tracking service.
|
||||
* Provides functionality to track and retrieve user page visit history.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const visitsApiRef = createApiRef<VisitsApi>({
|
||||
id: 'homepage.visits',
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
Reference in New Issue
Block a user