From f6a5144861a87e63b5e91d47b92940f53da25ef6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Mar 2026 16:52:45 +0100 Subject: [PATCH] update NFS header migration scope Roll back the TechDocs, catalog graph, and catalog import migrations for now while keeping the shared sub-page routing fix and the page-level header cleanup for search, notifications, and catalog entity pages. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .changeset/nfs-header-page-migrations.md | 3 - .../src/blueprints/PageBlueprint.test.tsx | 48 +++- .../src/blueprints/PageBlueprint.tsx | 43 ++-- plugins/catalog-graph/src/alpha.tsx | 4 +- .../CatalogGraphPage/CatalogGraphPage.tsx | 224 ++++++++---------- plugins/catalog-import/package.json | 1 - plugins/catalog-import/src/alpha.tsx | 4 +- .../DefaultImportPage/DefaultImportPage.tsx | 56 +---- .../src/components/ImportPage/ImportPage.tsx | 7 - .../components/EntityHeader/EntityHeader.tsx | 100 ++++---- .../NotificationsPage/NotificationsPage.tsx | 8 +- plugins/search/src/alpha.tsx | 200 ++++++++-------- plugins/techdocs/package.json | 1 - .../src/alpha/NfsTechDocsIndexPage.test.tsx | 79 ------ .../src/alpha/NfsTechDocsIndexPage.tsx | 51 ---- .../src/alpha/NfsTechDocsReaderLayout.tsx | 205 ---------------- plugins/techdocs/src/alpha/index.tsx | 12 +- .../TechDocsReaderPage/TechDocsReaderPage.tsx | 1 - 18 files changed, 343 insertions(+), 704 deletions(-) delete mode 100644 plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx delete mode 100644 plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx delete mode 100644 plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx diff --git a/.changeset/nfs-header-page-migrations.md b/.changeset/nfs-header-page-migrations.md index 135a77c015..d8d6cb04cd 100644 --- a/.changeset/nfs-header-page-migrations.md +++ b/.changeset/nfs-header-page-migrations.md @@ -2,13 +2,10 @@ '@backstage/plugin-api-docs': patch '@backstage/plugin-auth': patch '@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-graph': patch -'@backstage/plugin-catalog-import': patch '@backstage/plugin-catalog-unprocessed-entities': patch '@backstage/plugin-devtools': patch '@backstage/plugin-notifications': patch '@backstage/plugin-search': patch -'@backstage/plugin-techdocs': patch '@backstage/plugin-user-settings': patch --- diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx index 925c370af6..4561c8db59 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -24,7 +24,7 @@ import { createExtensionBlueprint, createExtensionInput, } from '../wiring'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; describe('PageBlueprint', () => { const mockRouteRef = createRouteRef(); @@ -279,4 +279,50 @@ describe('PageBlueprint', () => { } `); }); + + it('should resolve sub-page tab hrefs relative to the parent page', async () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + path: '/test', + routeRef: mockRouteRef, + title: 'Test', + }, + }); + + const SubPageBlueprint = createExtensionBlueprint({ + kind: 'sub-page', + attachTo: { id: 'page:test-page', input: 'pages' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.title.optional(), + ], + factory() { + return [ + coreExtensionData.routePath('config'), + coreExtensionData.title('Config'), + coreExtensionData.reactElement(
Config page
), + ]; + }, + }); + + const tester = createExtensionTester(myPage).add( + SubPageBlueprint.make({ name: 'config', params: {} }), + ); + + renderInTestApp(tester.reactElement(), { + mountedRoutes: { + '/test/*': mockRouteRef, + }, + initialRouteEntries: ['/test'], + }); + + await waitFor(() => + expect(screen.getByRole('tab', { name: 'Config' })).toHaveAttribute( + 'href', + '/test/config', + ), + ); + }); }); diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx index bf2e6d8135..7daf876ac1 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -15,7 +15,7 @@ */ import { JSX } from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; +import { Routes, Route, Navigate, useResolvedPath } from 'react-router-dom'; import { IconElement } from '../icons/types'; import { RouteRef } from '../routing'; import { @@ -72,6 +72,7 @@ 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; @@ -79,7 +80,7 @@ export const PageBlueprint = createExtensionBlueprint({ title ?? node.spec.plugin.title ?? node.spec.plugin.pluginId; const resolvedIcon = icon ?? node.spec.plugin.icon; - yield coreExtensionData.routePath(config.path ?? params.path); + yield coreExtensionData.routePath(routePath); if (params.loader) { const loader = params.loader; const PageContent = () => { @@ -99,24 +100,34 @@ export const PageBlueprint = createExtensionBlueprint({ }; yield coreExtensionData.reactElement(); } 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 ( - import('./components/CatalogGraphPage/CatalogGraphPage').then(m => ( - + import('./components/CatalogGraphPage').then(m => ( + )), }); }, diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index dadbb8d71d..7e6420da1f 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -23,7 +23,6 @@ import { SupportButton, } from '@backstage/core-components'; import { useAnalytics, useRouteRef } from '@backstage/core-plugin-api'; -import { HeaderPage } from '@backstage/ui'; import { entityRouteRef, humanizeEntityRef, @@ -117,23 +116,21 @@ const useStyles = makeStyles( { name: 'PluginCatalogGraphCatalogGraphPage' }, ); -type CatalogGraphPageProps = { - initialState?: { - selectedRelations?: string[]; - selectedKinds?: string[]; - rootEntityRefs?: string[]; - maxDepth?: number; - unidirectional?: boolean; - mergeRelations?: boolean; - direction?: Direction; - showFilters?: boolean; - curve?: 'curveStepBefore' | 'curveMonotoneX'; - }; -} & Partial; - -function CatalogGraphPageContent( - props: CatalogGraphPageProps & { headerVariant: 'legacy' | 'bui' }, -) { +export const CatalogGraphPage = ( + props: { + initialState?: { + selectedRelations?: string[]; + selectedKinds?: string[]; + rootEntityRefs?: string[]; + maxDepth?: number; + unidirectional?: boolean; + mergeRelations?: boolean; + direction?: Direction; + showFilters?: boolean; + curve?: 'curveStepBefore' | 'curveMonotoneX'; + }; + } & Partial, +) => { const { relationPairs, initialState, entityFilter } = props; const { t } = useTranslationRef(catalogGraphTranslationRef); const navigate = useNavigate(); @@ -188,124 +185,93 @@ function CatalogGraphPageContent( [catalogEntityRoute, navigate, setRootEntityNames, analytics], ); - const pageBody = ( - - {showFilters && ( - - - - - - - - - - )} - - - - {' '} - {t('catalogGraphPage.zoomOutDescription')} - - 0 - ? selectedKinds - : undefined - } - relations={ - selectedRelations && selectedRelations.length > 0 - ? selectedRelations - : undefined - } - mergeRelations={mergeRelations} - unidirectional={unidirectional} - onNodeClick={onNodeClick} - direction={direction} - relationPairs={relationPairs} - entityFilter={entityFilter} - className={classes.graph} - zoom="enabled" - curve={curve} - /> - - - - ); - const filterAction = ( - toggleShowFilters()} - > - {t('catalogGraphPage.filterToggleButtonTitle')} - - ); - const headerActions = ( - <> - {filterAction} - - {t('catalogGraphPage.supportButtonDescription')} - - - ); - const subtitle = rootEntityNames.map(e => humanizeEntityRef(e)).join(', '); - - if (props.headerVariant === 'bui') { - return ( - <> - - - {pageBody} - - - ); - } - return ( -
+
humanizeEntityRef(e)).join(', ')} + /> - + toggleShowFilters()} + > + {t('catalogGraphPage.filterToggleButtonTitle')} + + } + > {t('catalogGraphPage.supportButtonDescription')} - {pageBody} + + {showFilters && ( + + + + + + + + + + )} + + + + {' '} + {t('catalogGraphPage.zoomOutDescription')} + + 0 + ? selectedKinds + : undefined + } + relations={ + selectedRelations && selectedRelations.length > 0 + ? selectedRelations + : undefined + } + mergeRelations={mergeRelations} + unidirectional={unidirectional} + onNodeClick={onNodeClick} + direction={direction} + relationPairs={relationPairs} + entityFilter={entityFilter} + className={classes.graph} + zoom="enabled" + curve={curve} + /> + + + ); -} - -export const CatalogGraphPage = (props: CatalogGraphPageProps) => ( - -); - -export const NfsCatalogGraphPage = (props: CatalogGraphPageProps) => ( - -); +}; diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 69637f30ef..21ec443d00 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -66,7 +66,6 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", - "@backstage/ui": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/plugins/catalog-import/src/alpha.tsx b/plugins/catalog-import/src/alpha.tsx index 73fc73c8df..a00eb39988 100644 --- a/plugins/catalog-import/src/alpha.tsx +++ b/plugins/catalog-import/src/alpha.tsx @@ -43,9 +43,9 @@ const catalogImportPage = PageBlueprint.make({ path: '/catalog-import', routeRef: rootRouteRef, loader: () => - import('./components/ImportPage/ImportPage').then(m => ( + import('./components/ImportPage').then(m => ( - + )), }, diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index 55d218e03b..24a79866b3 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -23,7 +23,6 @@ import { } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; -import { HeaderPage } from '@backstage/ui'; import Grid from '@material-ui/core/Grid'; import { useTheme } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; @@ -53,22 +52,17 @@ export const DefaultImportPage = () => { , ]; - const headerTitle = t('defaultImportPage.headerTitle'); - const supportAction = ( - - {t('defaultImportPage.supportTitle', { appTitle })} - - ); - const contentHeaderTitle = t('defaultImportPage.contentHeaderTitle', { - appTitle, - }); return ( -
+
- - {supportAction} + + + {t('defaultImportPage.supportTitle', { appTitle })} + @@ -78,39 +72,3 @@ export const DefaultImportPage = () => { ); }; - -export const NfsDefaultImportPage = () => { - const { t } = useTranslationRef(catalogImportTranslationRef); - const theme = useTheme(); - const configApi = useApi(configApiRef); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - const appTitle = configApi.getOptionalString('app.title') || 'Backstage'; - - const contentItems = [ - - - , - - - - , - ]; - - return ( - <> - - {t('defaultImportPage.supportTitle', { appTitle })} - - } - /> - - - {isMobile ? contentItems : contentItems.reverse()} - - - - ); -}; diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx index 7dbc9acdc0..4a8c16e4c7 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx @@ -16,7 +16,6 @@ import { useOutlet } from 'react-router-dom'; import { DefaultImportPage } from '../DefaultImportPage'; -import { NfsDefaultImportPage } from '../DefaultImportPage/DefaultImportPage'; /** * The whole catalog import page. @@ -28,9 +27,3 @@ export const ImportPage = () => { return outlet || ; }; - -export const NfsImportPage = () => { - const outlet = useOutlet(); - - return outlet || ; -}; diff --git a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx index 543da69099..75e0baa113 100644 --- a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx +++ b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx @@ -27,14 +27,13 @@ import useAsync from 'react-use/esm/useAsync'; import { makeStyles } from '@material-ui/core/styles'; import Box from '@material-ui/core/Box'; -import { Breadcrumbs } from '@backstage/core-components'; +import { Header, Breadcrumbs } from '@backstage/core-components'; import { useApi, useRouteRef, useRouteRefParams, } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/frontend-plugin-api'; -import { HeaderPage } from '@backstage/ui'; import { Entity, @@ -49,6 +48,7 @@ import { EntityRefLink, InspectEntityDialog, UnregisterEntityDialog, + EntityDisplayName, FavoriteEntity, } from '@backstage/plugin-catalog-react'; @@ -57,11 +57,12 @@ import { EntityContextMenu } from '../../../components/EntityContextMenu'; import { rootRouteRef, unregisterRedirectRouteRef } from '../../../routes'; function headerProps( - _paramKind: string | undefined, + paramKind: string | undefined, paramNamespace: string | undefined, paramName: string | undefined, entity: Entity | undefined, -): { headerTitle: string } { +): { headerTitle: string; headerType: string } { + const kind = paramKind ?? entity?.kind ?? ''; const namespace = paramNamespace ?? entity?.metadata.namespace ?? ''; const name = entity?.metadata.title ?? paramName ?? entity?.metadata.name ?? ''; @@ -70,6 +71,14 @@ function headerProps( headerTitle: `${name}${ namespace && namespace !== DEFAULT_NAMESPACE ? ` in ${namespace}` : '' }`, + headerType: (() => { + let t = kind.toLocaleLowerCase('en-US'); + if (entity && entity.spec && 'type' in entity.spec) { + t += ' — '; + t += (entity.spec as { type: string }).type.toLocaleLowerCase('en-US'); + } + return t; + })(), }; } @@ -103,12 +112,30 @@ const useStyles = makeStyles(theme => ({ }, })); +function EntityHeaderTitle() { + const { entity } = useAsyncEntity(); + const { kind, namespace, name } = useRouteRefParams(entityRouteRef); + const { headerTitle: title } = headerProps(kind, namespace, name, entity); + return ( + + + {entity ? : title} + + {entity && } + + ); +} + function EntityHeaderSubtitle(props: { parentEntityRelations?: string[] }) { const { parentEntityRelations } = props; const classes = useStyles(); const { entity } = useAsyncEntity(); const { name } = useRouteRefParams(entityRouteRef); - const entityName = entity?.metadata.title ?? name; const parentEntity = findParentRelation( entity?.relations ?? [], parentEntityRelations ?? [], @@ -132,7 +159,7 @@ function EntityHeaderSubtitle(props: { parentEntityRelations?: string[] }) { )} - {entityName} + {name} ) : null; } @@ -176,7 +203,12 @@ export function EntityHeader(props: { } = props; const { entity } = useAsyncEntity(); const { kind, namespace, name } = useRouteRefParams(entityRouteRef); - const { headerTitle } = headerProps(kind, namespace, name, entity); + const { headerTitle: entityFallbackText, headerType: type } = headerProps( + kind, + namespace, + name, + entity, + ); const location = useLocation(); const navigate = useNavigate(); @@ -233,44 +265,28 @@ export function EntityHeader(props: { ); const inspectDialogOpen = typeof selectedInspectEntityDialogTab === 'string'; - const customTitle = typeof title === 'string' ? undefined : title; - const headerSubtitle = subtitle ?? ( - - ); - const renderedTitle = typeof title === 'string' ? title : headerTitle; return ( - <> - - - - - ) : undefined - } - /> - {(customTitle || headerSubtitle || entity) && ( - - {customTitle} - {headerSubtitle} - {entity && ( - - - - )} - - )} +
} + subtitle={ + subtitle ?? ( + + ) + } + > {entity && ( <> + + )} - +
); } diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 61dcb15e5a..78ad4f14d0 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -21,7 +21,6 @@ import { PageWithHeader, ResponseErrorPanel, } from '@backstage/core-components'; -import { HeaderPage } from '@backstage/ui'; import Grid from '@material-ui/core/Grid'; import { ConfirmProvider } from 'material-ui-confirm'; import { useSignal } from '@backstage/plugin-signals-react'; @@ -233,12 +232,7 @@ function NotificationsPageContent( ); if (headerVariant === 'bui') { - return ( - <> - - {pageContent} - - ); + return pageContent; } return ( diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 3acad014a5..eb637502e6 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -25,7 +25,6 @@ import { DocsIcon, useSidebarPinState, } from '@backstage/core-components'; -import { HeaderPage } from '@backstage/ui'; import { useApi, discoveryApiRef, @@ -142,113 +141,110 @@ export const searchPage = PageBlueprint.makeWithOverrides({ const configApi = useApi(configApiRef); return ( - <> - {!isMobile && } - - - - - - {!isMobile && ( - - , - }, - { - value: 'techdocs', - name: 'Documentation', - icon: , - }, - ].concat(resultTypes)} - /> - - {types.includes('techdocs') && ( - { - // Return a list of entities which are documented. - const { items } = await catalogApi.getEntities({ - fields: ['metadata.name'], - filter: { - 'metadata.annotations.backstage.io/techdocs-ref': - CATALOG_FILTER_EXISTS, - }, - }); - - const names = items.map( - entity => entity.metadata.name, - ); - names.sort(); - return names; - }} - /> - )} + + + + + + {!isMobile && ( + + , + }, + { + value: 'techdocs', + name: 'Documentation', + icon: , + }, + ].concat(resultTypes)} + /> + + {types.includes('techdocs') && ( - - {additionalSearchFilters.map(SearchFilterComponent => ( - - ))} - - - )} - - - - {({ results }) => ( - <> - {results.map((result, index) => { - const { noTrack } = config; - const { document, ...rest } = result; - const SearchResultListItem = - getResultItemComponent(result); - return ( - + label="Entity" + name="name" + values={async () => { + // Return a list of entities which are documented. + const { items } = await catalogApi.getEntities({ + fields: ['metadata.name'], + filter: { + 'metadata.annotations.backstage.io/techdocs-ref': + CATALOG_FILTER_EXISTS, + }, + }); + + const names = items.map( + entity => entity.metadata.name, ); - })} - + names.sort(); + return names; + }} + /> )} - - + + + {additionalSearchFilters.map(SearchFilterComponent => ( + + ))} + + )} + + + + {({ results }) => ( + <> + {results.map((result, index) => { + const { noTrack } = config; + const { document, ...rest } = result; + const SearchResultListItem = + getResultItemComponent(result); + return ( + + ); + })} + + )} + + - - +
+
); }; diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 11b8631a5e..5ec3c79f7d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -75,7 +75,6 @@ "@backstage/plugin-techdocs-common": "workspace:^", "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/theme": "workspace:^", - "@backstage/ui": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx b/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx deleted file mode 100644 index bd39a7c267..0000000000 --- a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 deleted file mode 100644 index 3b2450d404..0000000000 --- a/plugins/techdocs/src/alpha/NfsTechDocsIndexPage.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { 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, - DefaultTechDocsHome, -} from '../home/components'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; - -const NfsTechDocsPageWrapper: FC<{ children?: ReactNode }> = ({ children }) => { - const configApi = useApi(configApiRef); - const generatedSubtitle = `Documentation available in ${ - configApi.getOptionalString('organization.name') ?? 'Backstage' - }`; - - return ( - - - {generatedSubtitle} - {children} - - ); -}; - -export const NfsTechDocsIndexPage = (props: TechDocsIndexPageProps) => { - const outlet = useOutlet(); - - return ( - outlet || ( - - ) - ); -}; diff --git a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx b/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx deleted file mode 100644 index 724545a7a2..0000000000 --- a/plugins/techdocs/src/alpha/NfsTechDocsReaderLayout.tsx +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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 { PropsWithChildren, useEffect } from 'react'; -import Helmet from 'react-helmet'; -import Grid from '@material-ui/core/Grid'; -import Box from '@material-ui/core/Box'; -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, Page } from '@backstage/core-components'; -import { HeaderPage } from '@backstage/ui'; -import { - useTechDocsAddons, - TechDocsAddonLocations as locations, - useTechDocsReaderPage, -} from '@backstage/plugin-techdocs-react'; -import { - entityPresentationApiRef, - EntityRefLink, - EntityRefLinks, - getEntityRelations, -} from '@backstage/plugin-catalog-react'; -import { - RELATION_OWNED_BY, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { TechDocsReaderPageContent } from '../reader/components/TechDocsReaderPageContent'; -import { TechDocsReaderPageSubheader } from '../reader/components/TechDocsReaderPageSubheader'; -import { rootRouteRef } from '../routes'; - -const skeleton = ; - -const NfsTechDocsReaderPageHeader = (props: PropsWithChildren<{}>) => { - const { children } = props; - const addons = useTechDocsAddons(); - const configApi = useApi(configApiRef); - const entityPresentationApi = useApi(entityPresentationApiRef); - const docsRootLink = useRouteRef(rootRouteRef)(); - const { '*': path = '' } = useParams(); - - const { - title, - setTitle, - subtitle, - setSubtitle, - entityRef, - metadata: { value: metadata, loading: metadataLoading }, - entityMetadata: { value: entityMetadata, loading: entityMetadataLoading }, - } = useTechDocsReaderPage(); - - useEffect(() => { - if (!metadata) { - return; - } - - setTitle(metadata.site_name); - setSubtitle(() => { - let { site_description } = metadata; - if (!site_description || site_description === 'None') { - site_description = ''; - } - return site_description; - }); - }, [metadata, setTitle, setSubtitle]); - - const appTitle = configApi.getOptional('app.title') || 'Backstage'; - const { locationMetadata, spec } = entityMetadata || {}; - const lifecycle = spec?.lifecycle; - const ownedByRelations = entityMetadata - ? getEntityRelations(entityMetadata, RELATION_OWNED_BY) - : []; - const labels = ( - <> - - } - /> - {ownedByRelations.length > 0 && ( - - } - /> - )} - {lifecycle ? ( - - ) : null} - {locationMetadata && - locationMetadata.type !== 'dir' && - locationMetadata.type !== 'file' ? ( - - - - - - Source - - - } - url={locationMetadata.target} - /> - ) : null} - - ); - - const noEntMetadata = !entityMetadataLoading && entityMetadata === undefined; - const noTdMetadata = !metadataLoading && metadata === undefined; - if (noEntMetadata || noTdMetadata) { - return null; - } - - const stringEntityRef = stringifyEntityRef(entityRef); - const entityDisplayName = - entityPresentationApi.forEntity(stringEntityRef).snapshot.primaryTitle; - const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); - const normalizeAndSpace = (str: string) => - str.replace(/[-_]/g, ' ').split(' ').map(capitalize).join(' '); - - let techdocsTabTitleItems: string[] = []; - if (path !== '') { - techdocsTabTitleItems = removeTrailingSlash(path) - .split('/') - .map(normalizeAndSpace); - } - - const tabTitleItems = [entityDisplayName, ...techdocsTabTitleItems, appTitle]; - const tabTitle = tabTitleItems.join(' | '); - - return ( - <> - - {tabTitle} - - - {children} - {addons.renderComponentsByLocation(locations.Header)} - - } - /> - {(subtitle || - metadataLoading || - entityMetadataLoading || - entityMetadata) && ( - - {subtitle !== '' ? subtitle || skeleton : null} - {entityMetadata && {labels}} - - )} - - ); -}; - -export type NfsTechDocsReaderLayoutProps = { - withHeader?: boolean; - withSearch?: boolean; -}; - -export const NfsTechDocsReaderLayout = ( - props: NfsTechDocsReaderLayoutProps, -) => { - const { withSearch, withHeader = true } = props; - - return ( - - {withHeader && } - - - - ); -}; diff --git a/plugins/techdocs/src/alpha/index.tsx b/plugins/techdocs/src/alpha/index.tsx index 47bedd0ce3..1ec92edfd3 100644 --- a/plugins/techdocs/src/alpha/index.tsx +++ b/plugins/techdocs/src/alpha/index.tsx @@ -46,6 +46,7 @@ import { rootDocsRouteRef, rootRouteRef, } from '../routes'; +import { TechDocsReaderLayout } from '../reader'; import { TechDocsAddons, techdocsApiRef, @@ -139,7 +140,9 @@ const techDocsPage = PageBlueprint.make({ path: '/docs', routeRef: rootRouteRef, loader: () => - import('./NfsTechDocsIndexPage').then(m => ), + import('../home/components/TechDocsIndexPage').then(m => ( + + )), }, }); @@ -183,12 +186,9 @@ const techDocsReaderPage = PageBlueprint.makeWithOverrides({ ); }); - return Promise.all([ - import('../Router'), - import('./NfsTechDocsReaderLayout'), - ]).then(([{ TechDocsReaderRouter }, { NfsTechDocsReaderLayout }]) => ( + return import('../Router').then(({ TechDocsReaderRouter }) => ( - diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index dbbb091412..4a2b9e2002 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -140,7 +140,6 @@ export type TechDocsReaderLayoutProps = { */ export const TechDocsReaderLayout = (props: TechDocsReaderLayoutProps) => { const { withSearch, withHeader = true } = props; - return ( {withHeader && }