Merge pull request #34299 from backstage/rugvip/nav-item-gone

frontend-plugin-api: remove NavItemBlueprint
This commit is contained in:
Patrik Oldsberg
2026-05-19 14:44:58 +02:00
committed by GitHub
49 changed files with 197 additions and 964 deletions
@@ -1821,43 +1821,6 @@ export const microsoftAuthApiRef: ApiRef_2<
readonly $$type: '@backstage/ApiRef';
};
// @public @deprecated
export const NavItemBlueprint: ExtensionBlueprint_2<{
kind: 'nav-item';
params: {
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
};
output: ExtensionDataRef_2<
{
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
},
'core.nav-item.target',
{}
>;
inputs: {};
config: {
title: string | undefined;
};
configInput: {
title?: string | undefined;
};
dataRefs: {
target: ConfigurableExtensionDataRef_2<
{
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
},
'core.nav-item.target',
{}
>;
};
}>;
// @public (undocumented)
export const NotFoundErrorPage: {
(props: NotFoundErrorPageProps): JSX.Element | null;
@@ -1,98 +0,0 @@
/*
* Copyright 2024 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 { createExtensionTester } from '@backstage/frontend-test-utils';
import { createRouteRef } from '../routing';
import { NavItemBlueprint } from './NavItemBlueprint';
describe('NavItemBlueprint', () => {
const mockRouteRef = createRouteRef();
const MockIcon = () => null;
it('should return an extension with sensible defaults', () => {
const extension = NavItemBlueprint.make({
params: {
icon: MockIcon,
routeRef: mockRouteRef,
title: 'TEST',
},
});
expect(extension).toMatchInlineSnapshot(`
{
"$$type": "@backstage/ExtensionDefinition",
"T": undefined,
"attachTo": {
"id": "app/nav",
"input": "items",
},
"configSchema": {
"parse": [Function],
"schema": [Function],
},
"disabled": false,
"factory": [Function],
"if": undefined,
"inputs": {},
"kind": "nav-item",
"name": undefined,
"output": [
[Function],
],
"override": [Function],
"toString": [Function],
"version": "v2",
}
`);
});
it('should return the correct extension data', () => {
const extension = NavItemBlueprint.make({
params: {
icon: MockIcon,
routeRef: mockRouteRef,
title: 'TEST',
},
});
const tester = createExtensionTester(extension);
expect(tester.get(NavItemBlueprint.dataRefs.target)).toEqual({
title: 'TEST',
icon: MockIcon,
routeRef: mockRouteRef,
});
});
it('should allow overriding of the title using config', () => {
const extension = NavItemBlueprint.make({
params: {
icon: MockIcon,
routeRef: mockRouteRef,
title: 'TEST',
},
});
const tester = createExtensionTester(extension, {
config: { title: 'OVERRIDDEN' },
});
expect(tester.get(NavItemBlueprint.dataRefs.target)).toEqual({
title: 'OVERRIDDEN',
icon: MockIcon,
routeRef: mockRouteRef,
});
});
});
@@ -1,66 +0,0 @@
/*
* Copyright 2024 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 { z } from 'zod/v4';
import { IconComponent } from '../icons/types';
import { RouteRef } from '../routing';
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
// TODO(Rugvip): Should this be broken apart into separate refs? title/icon/routeRef
const targetDataRef = createExtensionDataRef<{
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
}>().with({ id: 'core.nav-item.target' });
/**
* Creates extensions that make up the items of the nav bar.
*
* @public
* @deprecated Nav items are now automatically inferred from `PageBlueprint`
* extensions based on their `title` and `icon` params. You can remove your
* `NavItemBlueprint` usage and instead pass `title` and `icon` directly to
* the `PageBlueprint`.
*/
export const NavItemBlueprint = createExtensionBlueprint({
kind: 'nav-item',
attachTo: { id: 'app/nav', input: 'items' },
output: [targetDataRef],
dataRefs: {
target: targetDataRef,
},
factory: (
{
icon,
routeRef,
title,
}: {
title: string;
icon: IconComponent;
routeRef: RouteRef<undefined>;
},
{ config },
) => [
targetDataRef({
title: config.title ?? title,
icon,
routeRef,
}),
],
configSchema: {
title: z.string().optional(),
},
});
@@ -20,7 +20,6 @@ export {
} from './AnalyticsImplementationBlueprint';
export { ApiBlueprint } from './ApiBlueprint';
export { AppRootElementBlueprint } from './AppRootElementBlueprint';
export { NavItemBlueprint } from './NavItemBlueprint';
export { PageBlueprint } from './PageBlueprint';
export { SubPageBlueprint } from './SubPageBlueprint';
export { PluginHeaderActionBlueprint } from './PluginHeaderActionBlueprint';
@@ -15,7 +15,7 @@
*/
import { Fragment } from 'react';
import { Link, MemoryRouter } from 'react-router-dom';
import { MemoryRouter } from 'react-router-dom';
import { prepareSpecializedApp } from '@backstage/frontend-app-api';
import { RenderResult, render } from '@testing-library/react';
import { ConfigReader } from '@backstage/config';
@@ -25,9 +25,6 @@ import {
ExtensionDefinition,
coreExtensionData,
RouteRef,
useRouteRef,
IconComponent,
NavItemBlueprint,
createFrontendPlugin,
FrontendFeature,
createFrontendModule,
@@ -103,25 +100,6 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
apis?: readonly [...TestApiPairs<TApiPairs>];
};
const NavItem = (props: {
routeRef: RouteRef<undefined>;
title: string;
icon: IconComponent;
}) => {
const { routeRef, title, icon: Icon } = props;
const link = useRouteRef(routeRef);
if (!link) {
return null;
}
return (
<li>
<Link to={link()}>
<Icon /> {title}
</Link>
</li>
);
};
const appPluginOverride = appPlugin.withOverrides({
extensions: [
appPlugin.getExtension('sign-in-page:app').override({
@@ -134,33 +112,7 @@ const appPluginOverride = appPlugin.withOverrides({
disabled: true,
}),
appPlugin.getExtension('app/nav').override({
output: [coreExtensionData.reactElement],
factory(_originalFactory, { inputs }) {
return [
coreExtensionData.reactElement(
<nav>
<ul>
{inputs.items.map(
(item: (typeof inputs.items)[number], index: number) => {
const { icon, title, routeRef } = item.get(
NavItemBlueprint.dataRefs.target,
);
return (
<NavItem
key={index}
icon={icon}
title={title}
routeRef={routeRef}
/>
);
},
)}
</ul>
</nav>,
),
];
},
disabled: true,
}),
],
});