refactor(home): restructure extension architecture to match catalog pattern
Reorganize the home plugin's new frontend system extensions to follow the same pattern as the catalog plugin's EntityContentLayoutBlueprint. Key changes: - Widgets now attach directly to page:home instead of through an intermediate HomepageBlueprint extension - Add HomePageLayoutBlueprint in home-react for custom layout creation, with a built-in default layout as fallback - Remove compatWrapper from HomepageWidgetBlueprint (consumer responsibility, reduced need with NFS support in old system) - Remove HomepageCompositionRoot usage (old system artifact) - Delete HomepageBlueprint (replaced by HomePageLayoutBlueprint) - Update app-next example and dev harness to use new pattern Signed-off-by: Adam Kunicki <kunickiaj@gmail.com>
This commit is contained in:
@@ -20,11 +20,8 @@ import notFoundErrorPage from './examples/notFoundErrorPageExtension';
|
||||
import userSettingsPlugin from '@backstage/plugin-user-settings/alpha';
|
||||
import homePlugin from '@backstage/plugin-home/alpha';
|
||||
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createFrontendModule,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createFrontendModule } from '@backstage/frontend-plugin-api';
|
||||
import { HomePageLayoutBlueprint } from '@backstage/plugin-home-react/alpha';
|
||||
import {
|
||||
techdocsPlugin,
|
||||
TechDocsIndexPage,
|
||||
@@ -99,15 +96,14 @@ const convertedTechdocsPlugin = convertLegacyPlugin(techdocsPlugin, {
|
||||
const customHomePageModule = createFrontendModule({
|
||||
pluginId: 'home',
|
||||
extensions: [
|
||||
createExtension({
|
||||
name: 'my-home-page',
|
||||
attachTo: { id: 'page:home', input: 'props' },
|
||||
output: [coreExtensionData.reactElement, coreExtensionData.title],
|
||||
factory() {
|
||||
return [
|
||||
coreExtensionData.reactElement(homePage),
|
||||
coreExtensionData.title('just a title'),
|
||||
];
|
||||
HomePageLayoutBlueprint.make({
|
||||
params: {
|
||||
loader: async () =>
|
||||
// This is a legacy-style home page that renders static content.
|
||||
// In production, you'd use the widgets prop to render dynamic widgets.
|
||||
function LegacyHomePageLayout() {
|
||||
return homePage;
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { ReactElement } from 'react';
|
||||
import { RJSFSchema } from '@rjsf/utils';
|
||||
import { TranslationRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -39,6 +40,44 @@ export type ComponentParts = {
|
||||
ContextProvider?: (props: any) => JSX.Element;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
export const HomePageLayoutBlueprint: ExtensionBlueprint<{
|
||||
kind: 'home-page-layout';
|
||||
params: HomePageLayoutBlueprintParams;
|
||||
output: ExtensionDataRef<
|
||||
(props: HomePageLayoutProps) => JSX_2.Element,
|
||||
'home.layout.component',
|
||||
{}
|
||||
>;
|
||||
inputs: {};
|
||||
config: {};
|
||||
configInput: {};
|
||||
dataRefs: {
|
||||
component: ConfigurableExtensionDataRef<
|
||||
(props: HomePageLayoutProps) => JSX_2.Element,
|
||||
'home.layout.component',
|
||||
{}
|
||||
>;
|
||||
};
|
||||
}>;
|
||||
|
||||
// @alpha
|
||||
export interface HomePageLayoutBlueprintParams {
|
||||
loader: () => Promise<(props: HomePageLayoutProps) => JSX_2.Element>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export const homePageLayoutComponentDataRef: ConfigurableExtensionDataRef<
|
||||
(props: HomePageLayoutProps) => JSX_2.Element,
|
||||
'home.layout.component',
|
||||
{}
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export interface HomePageLayoutProps {
|
||||
widgets: Array<HomePageWidgetData>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export const HomepageWidgetBlueprint: ExtensionBlueprint<{
|
||||
kind: 'home-widget';
|
||||
|
||||
@@ -28,8 +28,14 @@ export {
|
||||
HomepageWidgetBlueprint,
|
||||
type HomepageWidgetBlueprintParams,
|
||||
} from './alpha/blueprints/HomepageWidgetBlueprint';
|
||||
export {
|
||||
HomePageLayoutBlueprint,
|
||||
type HomePageLayoutBlueprintParams,
|
||||
} from './alpha/blueprints/HomePageLayoutBlueprint';
|
||||
export {
|
||||
homePageWidgetDataRef,
|
||||
type HomePageWidgetData,
|
||||
homePageLayoutComponentDataRef,
|
||||
type HomePageLayoutProps,
|
||||
} from './alpha/dataRefs';
|
||||
export type { ComponentParts, CardLayout, CardSettings } from './extensions';
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { JSX } from 'react';
|
||||
import {
|
||||
createExtensionBlueprint,
|
||||
ExtensionBoundary,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
homePageLayoutComponentDataRef,
|
||||
HomePageLayoutProps,
|
||||
} from '../dataRefs';
|
||||
|
||||
/**
|
||||
* Parameters for creating a home page layout extension.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface HomePageLayoutBlueprintParams {
|
||||
/**
|
||||
* Async loader that returns the layout component.
|
||||
* The component receives the collected widgets and renders them.
|
||||
*/
|
||||
loader: () => Promise<(props: HomePageLayoutProps) => JSX.Element>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blueprint for creating custom home page layouts.
|
||||
*
|
||||
* A layout receives the list of installed widgets and is responsible for
|
||||
* arranging them on the home page. This follows the same pattern as
|
||||
* `EntityContentLayoutBlueprint` in the catalog plugin.
|
||||
*
|
||||
* If no layout extension is installed, the home page uses a built-in default.
|
||||
* Users can install their own layout to customize how widgets are arranged.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const HomePageLayoutBlueprint = createExtensionBlueprint({
|
||||
kind: 'home-page-layout',
|
||||
attachTo: { id: 'page:home', input: 'layouts' },
|
||||
output: [homePageLayoutComponentDataRef],
|
||||
dataRefs: {
|
||||
component: homePageLayoutComponentDataRef,
|
||||
},
|
||||
*factory({ loader }: HomePageLayoutBlueprintParams, { node }) {
|
||||
yield homePageLayoutComponentDataRef(
|
||||
ExtensionBoundary.lazyComponent(node, loader),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { lazy, ReactElement } from 'react';
|
||||
import { compatWrapper } from '@backstage/core-compat-api';
|
||||
import {
|
||||
createExtensionBlueprint,
|
||||
ExtensionBoundary,
|
||||
@@ -96,12 +95,11 @@ export const HomepageWidgetBlueprint = createExtensionBlueprint({
|
||||
|
||||
const Widget = (
|
||||
props: CardExtensionProps<Record<string, unknown>>,
|
||||
): ReactElement =>
|
||||
compatWrapper(
|
||||
<ExtensionBoundary node={node}>
|
||||
<LazyCard {...props} />
|
||||
</ExtensionBoundary>,
|
||||
);
|
||||
): ReactElement => (
|
||||
<ExtensionBoundary node={node}>
|
||||
<LazyCard {...props} />
|
||||
</ExtensionBoundary>
|
||||
);
|
||||
|
||||
yield homePageWidgetDataRef({
|
||||
component: <Widget {...(params.componentProps ?? {})} />,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { createExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ReactElement } from 'react';
|
||||
import { JSX, ReactElement } from 'react';
|
||||
import type { CardLayout, CardSettings } from '../extensions';
|
||||
|
||||
/**
|
||||
@@ -64,3 +64,30 @@ export const homePageWidgetDataRef =
|
||||
createExtensionDataRef<HomePageWidgetData>().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<HomePageWidgetData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
});
|
||||
|
||||
+33
-23
@@ -21,17 +21,20 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import catalogPlugin from '@backstage/plugin-catalog/alpha';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import {
|
||||
ApiBlueprint,
|
||||
createFrontendModule,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { HomepageWidgetBlueprint } from '@backstage/plugin-home-react/alpha';
|
||||
import {
|
||||
HomepageWidgetBlueprint,
|
||||
HomePageLayoutBlueprint,
|
||||
} from '@backstage/plugin-home-react/alpha';
|
||||
import { HeaderWorldClock, WelcomeTitle, type ClockConfig } from '../src';
|
||||
import homePlugin, {
|
||||
HomepageBlueprint,
|
||||
type HomepageGridProps,
|
||||
} from '../src/alpha';
|
||||
import homePlugin from '../src/alpha';
|
||||
import { CustomHomepageGrid } from '../src/components';
|
||||
import type { LayoutConfiguration } from '../src/components/CustomHomepage/types';
|
||||
|
||||
const clockConfigs: ClockConfig[] = [
|
||||
{ label: 'NYC', timeZone: 'America/New_York' },
|
||||
@@ -46,7 +49,7 @@ const timeFormat: Intl.DateTimeFormatOptions = {
|
||||
hour12: false,
|
||||
};
|
||||
|
||||
const defaultGridConfig: NonNullable<HomepageGridProps['config']> = [
|
||||
const defaultGridConfig: LayoutConfiguration[] = [
|
||||
{
|
||||
component: 'HomePageToolkit',
|
||||
x: 0,
|
||||
@@ -72,23 +75,30 @@ const defaultGridConfig: NonNullable<HomepageGridProps['config']> = [
|
||||
},
|
||||
];
|
||||
|
||||
const homePage = HomepageBlueprint.make({
|
||||
const homePageLayout = HomePageLayoutBlueprint.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>
|
||||
),
|
||||
loader: async () =>
|
||||
function CustomHomePageLayout({ widgets }) {
|
||||
return (
|
||||
<Page themeId="home">
|
||||
<Header title={<WelcomeTitle />} pageTitleOverride="Home">
|
||||
<HeaderWorldClock
|
||||
clockConfigs={clockConfigs}
|
||||
customTimeFormat={timeFormat}
|
||||
/>
|
||||
</Header>
|
||||
<Content>
|
||||
<CustomHomepageGrid config={defaultGridConfig}>
|
||||
{widgets.map((widget, index) => (
|
||||
<Fragment key={widget.name ?? index}>
|
||||
{widget.component}
|
||||
</Fragment>
|
||||
))}
|
||||
</CustomHomepageGrid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -163,7 +173,7 @@ const homePageRandomJokeWidget = HomepageWidgetBlueprint.make({
|
||||
const homeDevModule = createFrontendModule({
|
||||
pluginId: 'home',
|
||||
extensions: [
|
||||
homePage,
|
||||
homePageLayout,
|
||||
homePageToolkitWidget,
|
||||
homePageStarredEntitiesWidget,
|
||||
homePageRandomJokeWidget,
|
||||
|
||||
@@ -7,18 +7,16 @@ import { AnyApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
|
||||
import { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { CSSProperties } from 'react';
|
||||
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInput } from '@backstage/frontend-plugin-api';
|
||||
import { HomePageLayoutProps } from '@backstage/plugin-home-react/alpha';
|
||||
import { HomePageWidgetData } from '@backstage/plugin-home-react/alpha';
|
||||
import { IconComponent } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api';
|
||||
import { ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
@@ -97,24 +95,27 @@ const _default: OverridableFrontendPlugin<
|
||||
}
|
||||
>;
|
||||
inputs: {
|
||||
props: ExtensionInput<
|
||||
| ConfigurableExtensionDataRef<
|
||||
JSX_2.Element,
|
||||
'core.reactElement',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>
|
||||
| ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'core.title',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>,
|
||||
widgets: ExtensionInput<
|
||||
ConfigurableExtensionDataRef<
|
||||
HomePageWidgetData,
|
||||
'home.widget.data',
|
||||
{}
|
||||
>,
|
||||
{
|
||||
singleton: true;
|
||||
optional: true;
|
||||
singleton: false;
|
||||
optional: false;
|
||||
internal: false;
|
||||
}
|
||||
>;
|
||||
layouts: ExtensionInput<
|
||||
ConfigurableExtensionDataRef<
|
||||
(props: HomePageLayoutProps) => JSX_2.Element,
|
||||
'home.layout.component',
|
||||
{}
|
||||
>,
|
||||
{
|
||||
singleton: false;
|
||||
optional: false;
|
||||
internal: false;
|
||||
}
|
||||
>;
|
||||
@@ -132,69 +133,10 @@ const _default: OverridableFrontendPlugin<
|
||||
>;
|
||||
export default _default;
|
||||
|
||||
// @alpha
|
||||
export const HomepageBlueprint: ExtensionBlueprint<{
|
||||
kind: 'home-page';
|
||||
params: HomepageBlueprintParams;
|
||||
output:
|
||||
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
|
||||
| ExtensionDataRef<
|
||||
string,
|
||||
'core.title',
|
||||
{
|
||||
optional: true;
|
||||
}
|
||||
>;
|
||||
inputs: {
|
||||
widgets: ExtensionInput<
|
||||
ConfigurableExtensionDataRef<HomePageWidgetData, 'home.widget.data', {}>,
|
||||
{
|
||||
singleton: false;
|
||||
optional: false;
|
||||
internal: 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',
|
||||
{
|
||||
readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!';
|
||||
readonly 'addWidgetDialog.title': 'Add new widget to dashboard';
|
||||
readonly 'customHomepageButtons.cancel': 'Cancel';
|
||||
readonly 'customHomepageButtons.clearAll': 'Clear all';
|
||||
@@ -203,10 +145,10 @@ export const homeTranslationRef: TranslationRef<
|
||||
readonly 'customHomepageButtons.addWidget': 'Add widget';
|
||||
readonly 'customHomepageButtons.save': 'Save';
|
||||
readonly 'customHomepage.noWidgets': "No widgets added. Start by clicking the 'Add widget' button.";
|
||||
readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel';
|
||||
readonly 'widgetSettingsOverlay.editSettingsTooptip': 'Edit settings';
|
||||
readonly 'widgetSettingsOverlay.deleteWidgetTooltip': 'Delete widget';
|
||||
readonly 'widgetSettingsOverlay.submitButtonTitle': 'Submit';
|
||||
readonly 'widgetSettingsOverlay.cancelButtonTitle': 'Cancel';
|
||||
readonly 'starredEntityListItem.removeFavoriteEntityTitle': 'Remove entity from favorites';
|
||||
readonly 'visitList.empty.title': 'There are no visits to show yet.';
|
||||
readonly 'visitList.empty.description': 'Once you start using Backstage, your visits will appear here as a quick link to carry on where you left off.';
|
||||
@@ -214,6 +156,7 @@ export const homeTranslationRef: TranslationRef<
|
||||
readonly 'quickStart.title': 'Onboarding';
|
||||
readonly 'quickStart.description': 'Get started with Backstage';
|
||||
readonly 'quickStart.learnMoreLinkTitle': 'Learn more';
|
||||
readonly 'starredEntities.noStarredEntitiesMessage': 'Click the star beside an entity name to add it to this list!';
|
||||
readonly 'visitedByType.action.viewMore': 'View more';
|
||||
readonly 'visitedByType.action.viewLess': 'View less';
|
||||
readonly 'featuredDocsCard.empty.title': 'No documents to show';
|
||||
|
||||
+42
-25
@@ -24,8 +24,8 @@
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
import { lazy as reactLazy } from 'react';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
PageBlueprint,
|
||||
NavItemBlueprint,
|
||||
@@ -36,37 +36,60 @@ import {
|
||||
storageApiRef,
|
||||
errorApiRef,
|
||||
ApiBlueprint,
|
||||
ExtensionBoundary,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { VisitListener } from './components/';
|
||||
import { visitsApiRef, VisitsStorageApi, VisitsWebStorageApi } from './api';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import {
|
||||
homePageWidgetDataRef,
|
||||
homePageLayoutComponentDataRef,
|
||||
HomePageLayoutBlueprint,
|
||||
type HomePageLayoutProps,
|
||||
} from '@backstage/plugin-home-react/alpha';
|
||||
|
||||
const rootRouteRef = createRouteRef();
|
||||
|
||||
const homePage = PageBlueprint.makeWithOverrides({
|
||||
inputs: {
|
||||
props: createExtensionInput(
|
||||
[
|
||||
coreExtensionData.reactElement.optional(),
|
||||
coreExtensionData.title.optional(),
|
||||
],
|
||||
{
|
||||
singleton: true,
|
||||
optional: true,
|
||||
},
|
||||
),
|
||||
widgets: createExtensionInput([homePageWidgetDataRef]),
|
||||
layouts: createExtensionInput([HomePageLayoutBlueprint.dataRefs.component]),
|
||||
},
|
||||
factory: (originalFactory, { inputs }) => {
|
||||
factory(originalFactory, { node, inputs }) {
|
||||
return originalFactory({
|
||||
path: '/home',
|
||||
routeRef: rootRouteRef,
|
||||
loader: () =>
|
||||
import('./components/').then(m => (
|
||||
<m.HomepageCompositionRoot
|
||||
children={inputs.props?.get(coreExtensionData.reactElement)}
|
||||
title={inputs.props?.get(coreExtensionData.title)}
|
||||
/>
|
||||
)),
|
||||
loader: async () => {
|
||||
const LazyDefaultLayout = reactLazy(() =>
|
||||
import('./alpha/DefaultHomePageLayout').then(m => ({
|
||||
default: m.DefaultHomePageLayout,
|
||||
})),
|
||||
);
|
||||
|
||||
const DefaultLayoutComponent = (props: HomePageLayoutProps) => (
|
||||
<ExtensionBoundary node={node}>
|
||||
<LazyDefaultLayout {...props} />
|
||||
</ExtensionBoundary>
|
||||
);
|
||||
|
||||
const layouts = [
|
||||
...inputs.layouts.map(layout => ({
|
||||
Component: layout.get(homePageLayoutComponentDataRef),
|
||||
})),
|
||||
{
|
||||
Component: DefaultLayoutComponent,
|
||||
},
|
||||
];
|
||||
|
||||
const widgets = inputs.widgets.map(widget =>
|
||||
widget.get(homePageWidgetDataRef),
|
||||
);
|
||||
|
||||
// Use the first installed layout, or fall back to the default
|
||||
const layout = layouts[0];
|
||||
|
||||
return <layout.Component widgets={widgets} />;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -126,12 +149,6 @@ export default createFrontendPlugin({
|
||||
});
|
||||
|
||||
export { homeTranslationRef } from './translation';
|
||||
export {
|
||||
HomepageBlueprint,
|
||||
type HomepageBlueprintParams,
|
||||
type HomepageTemplateProps,
|
||||
type HomepageGridProps,
|
||||
} from './alpha/HomepageBlueprint';
|
||||
export {
|
||||
type LayoutConfiguration,
|
||||
type Breakpoint,
|
||||
|
||||
@@ -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 `<CustomHomepageGrid/>` wrapped in `<Content/>`.
|
||||
* This is used when no custom layout extension is installed.
|
||||
*/
|
||||
export function DefaultHomePageLayout(props: HomePageLayoutProps) {
|
||||
const { widgets } = props;
|
||||
return (
|
||||
<Content>
|
||||
<CustomHomepageGrid>
|
||||
{widgets.map((widget, index) => (
|
||||
<Fragment key={widget.name ?? index}>{widget.component}</Fragment>
|
||||
))}
|
||||
</CustomHomepageGrid>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { compatWrapper } from '@backstage/core-compat-api';
|
||||
import { Content } from '@backstage/core-components';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtensionBlueprint,
|
||||
createExtensionInput,
|
||||
ExtensionBoundary,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Fragment, type ReactElement, type ReactNode } from 'react';
|
||||
import { CustomHomepageGrid } from '../components';
|
||||
import type { CustomHomepageGridProps } from '../components';
|
||||
import { homePageWidgetDataRef } from '@backstage/plugin-home-react/alpha';
|
||||
|
||||
/**
|
||||
* Arguments provided to the homepage renderer.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface HomepageTemplateProps {
|
||||
/**
|
||||
* React elements built from the installed homepage widgets.
|
||||
*/
|
||||
widgets: ReactNode[];
|
||||
/**
|
||||
* A <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, coreExtensionData.title.optional()],
|
||||
inputs: {
|
||||
widgets: createExtensionInput([homePageWidgetDataRef]),
|
||||
},
|
||||
*factory(params: HomepageBlueprintParams = {}, { inputs, node }) {
|
||||
const widgetOutputs = inputs.widgets ?? [];
|
||||
const widgetElements = widgetOutputs.map((widget, index) => (
|
||||
<Fragment key={index}>
|
||||
{widget.get(homePageWidgetDataRef).component}
|
||||
</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 coreExtensionData.title(params.title);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export type { CustomHomepageGridProps as HomepageGridProps } from '../components';
|
||||
Reference in New Issue
Block a user