From aa22de55f4745617a5bfafa2a89eb313831e10c4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 10:13:29 +0100 Subject: [PATCH] fix: restore NFS header migration regressions Restore page theming and header metadata for the migrated NFS pages so the new BUI headers preserve the same context and navigation as before. This also makes the DevTools landing tab deterministic and adds focused regression coverage for the scaffolder and TechDocs fixes. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../components/EntityLayout/EntityLayout.tsx | 5 +- plugins/devtools/src/alpha/plugin.tsx | 34 ++++++-- .../components/ScaffolderPageLayout.test.tsx | 50 ++++++++++++ .../src/components/ScaffolderPageLayout.tsx | 33 +++++++- .../src/alpha/NfsTechDocsIndexPage.test.tsx | 79 +++++++++++++++++++ .../src/alpha/NfsTechDocsIndexPage.tsx | 9 ++- .../src/alpha/NfsTechDocsReaderLayout.tsx | 13 +-- 7 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 plugins/scaffolder/src/components/ScaffolderPageLayout.test.tsx create mode 100644 plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx index 17c4693bde..895f36150f 100644 --- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx @@ -27,6 +27,7 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { Content, Link, + Page, Progress, WarningPanel, } from '@backstage/core-components'; @@ -149,7 +150,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { const { t } = useTranslationRef(catalogTranslationRef); return ( - <> + {header ?? ( { )} )} - + ); }; diff --git a/plugins/devtools/src/alpha/plugin.tsx b/plugins/devtools/src/alpha/plugin.tsx index 4d81d43e17..0144308069 100644 --- a/plugins/devtools/src/alpha/plugin.tsx +++ b/plugins/devtools/src/alpha/plugin.tsx @@ -16,6 +16,7 @@ import { createFrontendPlugin, + coreExtensionData, discoveryApiRef, fetchApiRef, ApiBlueprint, @@ -50,11 +51,34 @@ export const devToolsApi = ApiBlueprint.make({ }); /** @alpha */ -export const devToolsPage = PageBlueprint.make({ - params: { - path: '/devtools', - routeRef: rootRouteRef, - title: 'DevTools', +export const devToolsPage = PageBlueprint.makeWithOverrides({ + factory(originalFactory, { inputs }) { + const pages = [...inputs.pages].sort((left, right) => { + const leftPath = left.get(coreExtensionData.routePath); + const rightPath = right.get(coreExtensionData.routePath); + + if (leftPath === 'info' && rightPath !== 'info') { + return -1; + } + if (leftPath !== 'info' && rightPath === 'info') { + return 1; + } + + return 0; + }); + + return originalFactory( + { + path: '/devtools', + routeRef: rootRouteRef, + title: 'DevTools', + }, + { + inputs: { + pages, + }, + }, + ); }, }); diff --git a/plugins/scaffolder/src/components/ScaffolderPageLayout.test.tsx b/plugins/scaffolder/src/components/ScaffolderPageLayout.test.tsx new file mode 100644 index 0000000000..510d69d748 --- /dev/null +++ b/plugins/scaffolder/src/components/ScaffolderPageLayout.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import { ScaffolderPageLayout } from './ScaffolderPageLayout'; + +describe('ScaffolderPageLayout', () => { + it('preserves BUI titles, subtitles, and breadcrumb links', async () => { + await renderInTestApp( + + Task My template + + } + subtitle="Task ID: 123" + type="Scaffolder" + typeLink="/create" + > +
Body content
+
, + ); + + expect( + screen.getByRole('heading', { name: 'Task page' }), + ).toBeInTheDocument(); + expect(screen.getByText('My template')).toBeInTheDocument(); + expect(screen.getByText('Task ID: 123')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Scaffolder' })).toHaveAttribute( + 'href', + '/create', + ); + }); +}); diff --git a/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx index fafddb2590..31d7233435 100644 --- a/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPageLayout.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ +import Box from '@material-ui/core/Box'; import { Content, Header, Page } from '@backstage/core-components'; import { HeaderPage } from '@backstage/ui'; import type { ReactNode } from 'react'; +import { useEffect } from 'react'; type HeaderVariant = 'legacy' | 'bui'; @@ -55,19 +57,44 @@ export const ScaffolderPageLayout = (props: ScaffolderPageLayoutProps) => { children ); + useEffect(() => { + if (!pageTitleOverride) { + return; + } + + document.title = pageTitleOverride; + }, [pageTitleOverride]); + if (headerVariant === 'bui') { let buiTitle: string | undefined; if (typeof title === 'string') { buiTitle = title; + } else if (pageTitleOverride) { + buiTitle = pageTitleOverride; } else if (typeof subtitle === 'string') { buiTitle = subtitle; } + const breadcrumbs = + type && typeLink ? [{ label: type, href: typeLink }] : undefined; + const customTitle = typeof title === 'string' ? undefined : title; + const hasHeaderDetails = Boolean(customTitle) || Boolean(subtitle); + return ( - <> - + + + {hasHeaderDetails && ( + + {customTitle} + {subtitle && {subtitle}} + + )} {pageContent} - + ); } diff --git a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx new file mode 100644 index 0000000000..bd39a7c267 --- /dev/null +++ b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; +import { + MockStarredEntitiesApi, + catalogApiRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; +import { + TestApiRegistry, + mockApis, + renderInTestApp, +} from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import { rootDocsRouteRef } from '../routes'; +import { NfsTechDocsIndexPage } from './NfsTechDocsIndexPage'; + +const mockCatalogApi = catalogApiMock({ + entities: [ + { + apiVersion: 'version', + kind: 'User', + metadata: { + name: 'owned', + namespace: 'default', + }, + }, + ], +}); + +describe('NfsTechDocsIndexPage', () => { + it('renders the documentation heading and subtitle', async () => { + const apiRegistry = TestApiRegistry.from( + [catalogApiRef, mockCatalogApi], + [ + configApiRef, + mockApis.config({ + data: { organization: { name: 'My Company' } }, + }), + ], + [storageApiRef, mockApis.storage()], + [starredEntitiesApiRef, new MockStarredEntitiesApi()], + ); + + await renderInTestApp( + + + , + { + mountedRoutes: { + '/docs/:namespace/:kind/:name/*': rootDocsRouteRef, + }, + }, + ); + + expect( + await screen.findByRole('heading', { name: 'Documentation' }), + ).toBeInTheDocument(); + expect( + await screen.findByText(/Documentation available in My Company/i), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx index 31ffe55ca3..3b2450d404 100644 --- a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx +++ b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx @@ -15,7 +15,9 @@ */ import { FC, ReactNode } from 'react'; +import Box from '@material-ui/core/Box'; import { useOutlet } from 'react-router-dom'; +import { Page } from '@backstage/core-components'; import { HeaderPage } from '@backstage/ui'; import { TechDocsIndexPageProps, @@ -30,10 +32,11 @@ const NfsTechDocsPageWrapper: FC<{ children?: ReactNode }> = ({ children }) => { }`; return ( - <> - + + + {generatedSubtitle} {children} - + ); }; diff --git a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx index 1bbf5f8673..806d286ceb 100644 --- a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx +++ b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx @@ -22,7 +22,7 @@ import Skeleton from '@material-ui/lab/Skeleton'; import CodeIcon from '@material-ui/icons/Code'; import capitalize from 'lodash/capitalize'; import { useParams } from 'react-router-dom'; -import { HeaderLabel } from '@backstage/core-components'; +import { HeaderLabel, Page } from '@backstage/core-components'; import { HeaderPage } from '@backstage/ui'; import { useTechDocsAddons, @@ -39,9 +39,10 @@ import { RELATION_OWNED_BY, stringifyEntityRef, } from '@backstage/catalog-model'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; import { TechDocsReaderPageContent } from '../reader/components/TechDocsReaderPageContent'; import { TechDocsReaderPageSubheader } from '../reader/components/TechDocsReaderPageSubheader'; +import { rootDocsRouteRef } from '../routes'; const skeleton = ; @@ -50,6 +51,7 @@ const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => { const addons = useTechDocsAddons(); const configApi = useApi(configApiRef); const entityPresentationApi = useApi(entityPresentationApiRef); + const docsRootLink = useRouteRef(rootDocsRouteRef)(); const { '*': path = '' } = useParams(); const { @@ -161,7 +163,8 @@ const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => { {tabTitle} {children} @@ -193,10 +196,10 @@ export const NfsTechDocsReaderLayout = ( const { withSearch, withHeader = true } = props; return ( - <> + {withHeader && } - + ); };