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 && }