Merge pull request #33389 from backstage/rugvip/nfs-header-page-migration

plugins: migrate NFS pages to HeaderPage
This commit is contained in:
Patrik Oldsberg
2026-03-17 22:16:12 +01:00
committed by GitHub
39 changed files with 1049 additions and 476 deletions
+35 -8
View File
@@ -41,6 +41,7 @@ class NavItemBag implements NavContentNavItems {
readonly #items: NavContentNavItem[];
readonly #index: Map<string, NavContentNavItem>;
readonly #taken: Set<string>;
#restItems: NavContentNavItem[] | undefined;
constructor(items: NavContentNavItem[], taken?: Iterable<string>) {
this.#items = items;
@@ -50,14 +51,27 @@ class NavItemBag implements NavContentNavItems {
take(id: string): NavContentNavItem | undefined {
const item = this.#index.get(id);
if (item) {
if (item && !this.#taken.has(id)) {
this.#taken.add(id);
if (this.#restItems) {
const index = this.#restItems.findIndex(
restItem => restItem.node.spec.id === id,
);
if (index !== -1) {
this.#restItems.splice(index, 1);
}
}
}
return item;
}
rest(): NavContentNavItem[] {
return this.#items.filter(item => !this.#taken.has(item.node.spec.id));
if (!this.#restItems) {
this.#restItems = this.#items.filter(
item => !this.#taken.has(item.node.spec.id),
);
}
return this.#restItems;
}
clone(): NavContentNavItems {
@@ -65,19 +79,32 @@ class NavItemBag implements NavContentNavItems {
}
withComponent(Component: (props: NavContentNavItem) => JSX.Element) {
let renderedItems: JSX.Element[] | undefined;
return {
take: (id: string) => {
const item = this.take(id);
return item ? <Component {...item} /> : null;
if (item && renderedItems) {
const index = renderedItems.findIndex(
renderedItem => renderedItem.key === item.node.spec.id,
);
if (index !== -1) {
renderedItems.splice(index, 1);
}
}
return item ? <Component key={item.node.spec.id} {...item} /> : null;
},
rest: (options?: { sortBy?: 'title' }) => {
const items = this.rest();
if (options?.sortBy === 'title') {
items.sort((a, b) => a.title.localeCompare(b.title));
if (!renderedItems) {
if (options?.sortBy === 'title') {
items.sort((a, b) => a.title.localeCompare(b.title));
}
renderedItems = items.map(item => (
<Component key={item.node.spec.id} {...item} />
));
}
return items.map(item => (
<Component key={item.node.spec.id} {...item} />
));
return renderedItems;
},
};
}
+22 -17
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,31 +76,35 @@ 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 (tabsWithMatchStrategy) {
return (
<>
{!noHeader && (
<PluginHeader
title={title}
icon={icon}
tabs={tabsWithMatchStrategy}
customActions={headerActions}
/>
)}
{children}
</>
);
if (noHeader) {
return <>{children}</>;
}
return <>{children}</>;
return (
<>
<PluginHeader
title={title}
icon={icon}
tabs={resolvedTabs}
customActions={headerActions}
/>
{children}
</>
);
},
}),
});