From 7e42c62038a70804121090a35939743db350c914 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 7 Apr 2026 14:27:12 +0200 Subject: [PATCH 1/3] fix(app): add check for disabled nav items to discovery of pages Signed-off-by: Benjamin Janssens --- .changeset/grumpy-experts-leave.md | 5 +++++ plugins/app/src/extensions/AppNav.tsx | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/grumpy-experts-leave.md diff --git a/.changeset/grumpy-experts-leave.md b/.changeset/grumpy-experts-leave.md new file mode 100644 index 0000000000..7504fe6428 --- /dev/null +++ b/.changeset/grumpy-experts-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': patch +--- + +Fixed a regression that caused disabled nav items to appear in the navigation bar. diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx index 09203303e5..caeeb9df0c 100644 --- a/plugins/app/src/extensions/AppNav.tsx +++ b/plugins/app/src/extensions/AppNav.tsx @@ -188,6 +188,12 @@ function NavContentRenderer(props: { return []; } + const navItemNodeId = node.spec.id.replace(/^page:/, 'nav-item:'); + const navItemNode = tree.nodes.get(navItemNodeId); + if (navItemNode?.spec.disabled) { + return []; + } + const routeRef = node.instance.getData(coreExtensionData.routeRef); if (!routeRef) { return []; From 1ecbb1837ea47a4de2e17272c6f7979c239c90ff Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Tue, 7 Apr 2026 15:26:35 +0200 Subject: [PATCH 2/3] test(app): add tests Signed-off-by: Benjamin Janssens --- plugins/app/src/extensions/AppNav.test.tsx | 100 +++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 plugins/app/src/extensions/AppNav.test.tsx diff --git a/plugins/app/src/extensions/AppNav.test.tsx b/plugins/app/src/extensions/AppNav.test.tsx new file mode 100644 index 0000000000..387c75a87a --- /dev/null +++ b/plugins/app/src/extensions/AppNav.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2026 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 { screen, waitFor, within } from '@testing-library/react'; +import { renderTestApp } from '@backstage/frontend-test-utils'; +import { + PageBlueprint, + NavItemBlueprint, + createRouteRef, +} from '@backstage/frontend-plugin-api'; + +const DEFAULT_CONFIG = { + app: { baseUrl: 'http://localhost:3000' }, + backend: { baseUrl: 'http://localhost:7007' }, +}; + +const mockPage = PageBlueprint.make({ + name: 'my-plugin', + params: { + title: 'My Plugin', + icon: icon, + path: '/my-plugin', + routeRef: createRouteRef(), + }, +}); + +const mockNavItem = NavItemBlueprint.make({ + name: 'my-plugin', + params: { + title: 'My Plugin', + icon: () => icon, + routeRef: createRouteRef(), + }, +}); + +describe('AppNav', () => { + it('should show a nav item for a page with an enabled nav-item extension', async () => { + renderTestApp({ + extensions: [mockPage, mockNavItem], + config: DEFAULT_CONFIG, + }); + + await waitFor(() => { + expect( + within(screen.getByRole('navigation')).getByText('My Plugin'), + ).toBeInTheDocument(); + }); + }); + + it('should hide a nav item when its nav-item extension is disabled via config', async () => { + renderTestApp({ + extensions: [mockPage, mockNavItem], + config: { + ...DEFAULT_CONFIG, + app: { + ...DEFAULT_CONFIG.app, + extensions: [{ 'nav-item:test/my-plugin': false }], + }, + }, + }); + + await waitFor(() => { + expect( + within(screen.getByRole('navigation')).queryByText('My Plugin'), + ).not.toBeInTheDocument(); + }); + }); + + it('should still show a nav item for a page without a nav-item extension', async () => { + renderTestApp({ + extensions: [mockPage], + config: { + ...DEFAULT_CONFIG, + app: { + ...DEFAULT_CONFIG.app, + extensions: [{ 'nav-item:test/my-plugin': false }], + }, + }, + }); + + await waitFor(() => { + expect( + within(screen.getByRole('navigation')).getByText('My Plugin'), + ).toBeInTheDocument(); + }); + }); +}); From baba2c95b04ec244e3357ba9577f343f7177b6f1 Mon Sep 17 00:00:00 2001 From: Benjamin Janssens Date: Wed, 13 May 2026 15:33:40 +0200 Subject: [PATCH 3/3] chore(app): add todo Signed-off-by: Benjamin Janssens --- plugins/app/src/extensions/AppNav.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/app/src/extensions/AppNav.tsx b/plugins/app/src/extensions/AppNav.tsx index caeeb9df0c..0d6618fc8c 100644 --- a/plugins/app/src/extensions/AppNav.tsx +++ b/plugins/app/src/extensions/AppNav.tsx @@ -188,6 +188,8 @@ function NavContentRenderer(props: { return []; } + // TODO: Nav items are deprecated, and this code covers for a documented ability to use nav items still in config to disable pages in the sidebar. + // Remove this code once nav items are completely gone from the codebase. const navItemNodeId = node.spec.id.replace(/^page:/, 'nav-item:'); const navItemNode = tree.nodes.get(navItemNodeId); if (navItemNode?.spec.disabled) {