Move sub-page tab href resolution to PageLayout

Move the logic for resolving relative sub-page tab hrefs from
PageBlueprint into the app PageLayout component, where it belongs
as an app-level rendering concern.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-03-17 20:33:11 +01:00
parent fa260da6e7
commit 6211dd871a
2 changed files with 25 additions and 30 deletions
@@ -15,7 +15,7 @@
*/
import { JSX } from 'react';
import { Routes, Route, Navigate, useResolvedPath } from 'react-router-dom';
import { Routes, Route, Navigate } from 'react-router-dom';
import { IconElement } from '../icons/types';
import { RouteRef } from '../routing';
import {
@@ -72,7 +72,6 @@ export const PageBlueprint = createExtensionBlueprint({
{ config, node, inputs },
) {
const title = config.title ?? params.title;
const routePath = config.path ?? params.path;
const icon = params.icon;
const pluginId = node.spec.plugin.pluginId;
const noHeader = params.noHeader ?? false;
@@ -80,7 +79,7 @@ export const PageBlueprint = createExtensionBlueprint({
title ?? node.spec.plugin.title ?? node.spec.plugin.pluginId;
const resolvedIcon = icon ?? node.spec.plugin.icon;
yield coreExtensionData.routePath(routePath);
yield coreExtensionData.routePath(config.path ?? params.path);
if (params.loader) {
const loader = params.loader;
const PageContent = () => {
@@ -100,34 +99,24 @@ export const PageBlueprint = createExtensionBlueprint({
};
yield coreExtensionData.reactElement(<PageContent />);
} else if (inputs.pages.length > 0) {
// Parent page with sub-pages - render header with tabs
const tabs: PageLayoutTab[] = inputs.pages.map(page => {
const path = page.get(coreExtensionData.routePath);
const tabTitle = page.get(coreExtensionData.title);
const tabIcon = page.get(coreExtensionData.icon);
return {
id: path,
label: tabTitle || path,
icon: tabIcon,
href: path,
};
});
const PageContent = () => {
const firstPagePath = inputs.pages[0]?.get(coreExtensionData.routePath);
const headerActionsApi = useApi(pluginHeaderActionsApiRef);
const headerActions = headerActionsApi.getPluginHeaderActions(pluginId);
const parentPath = useResolvedPath('.').pathname.replace(/\/$/, '');
const staticParentPath =
routePath.startsWith('/') &&
!routePath.includes('/:') &&
!routePath.includes('*')
? routePath.replace(/\/$/, '')
: undefined;
const tabs: PageLayoutTab[] = inputs.pages.map(page => {
const path = page.get(coreExtensionData.routePath);
const tabTitle = page.get(coreExtensionData.title);
const tabIcon = page.get(coreExtensionData.icon);
const tabPath = path.replace(/^\/+/, '');
const basePath = staticParentPath ?? parentPath ?? '';
const href = path.startsWith('/')
? path
: `${basePath}/${tabPath}`.replace(/\/{2,}/g, '/');
return {
id: path,
label: tabTitle || path,
icon: tabIcon,
href,
};
});
return (
<PageLayout
+9 -3
View File
@@ -29,6 +29,7 @@ import {
import { PluginHeader } from '@backstage/ui';
import Button from '@material-ui/core/Button';
import { useMemo } from 'react';
import { useResolvedPath } from 'react-router-dom';
export const Progress = SwappableComponentBlueprint.make({
name: 'core-progress',
@@ -75,13 +76,18 @@ export const PageLayout = SwappableComponentBlueprint.make({
component: SwappablePageLayout,
loader: () => (props: PageLayoutProps) => {
const { title, icon, noHeader, headerActions, tabs, children } = props;
const tabsWithMatchStrategy = useMemo(
// TODO(Rugvip): Different solution to this path handling would be good
const parentPath = useResolvedPath('.').pathname.replace(/\/$/, '');
const resolvedTabs = useMemo(
() =>
tabs?.map(tab => ({
...tab,
href: tab.href.startsWith('/')
? tab.href
: `${parentPath}/${tab.href}`.replace(/\/{2,}/g, '/'),
matchStrategy: 'prefix' as const,
})),
[tabs],
[tabs, parentPath],
);
if (noHeader) {
@@ -93,7 +99,7 @@ export const PageLayout = SwappableComponentBlueprint.make({
<PluginHeader
title={title}
icon={icon}
tabs={tabsWithMatchStrategy}
tabs={resolvedTabs}
customActions={headerActions}
/>
{children}