From 9ddc60b220786129ff00d10931fd37fd8680184e Mon Sep 17 00:00:00 2001 From: John Philip Date: Mon, 17 Feb 2025 02:14:11 -0500 Subject: [PATCH] add url params to title of techdocs Signed-off-by: John Philip --- .../TechDocsReaderPageHeader.test.tsx | 65 +++++++++++++++++-- .../TechDocsReaderPageHeader.tsx | 34 +++++++++- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx index d2956ce919..f6b93f46e0 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.test.tsx @@ -16,7 +16,10 @@ import React from 'react'; import { CompoundEntityRef } from '@backstage/catalog-model'; -import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { + entityPresentationApiRef, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; import { techdocsApiRef, TechDocsReaderPageProvider, @@ -26,6 +29,7 @@ import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { rootRouteRef } from '../../../routes'; import { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader'; +import { waitFor } from '@testing-library/react'; const mockEntityMetadata = { locationMetadata: { @@ -48,6 +52,16 @@ const mockTechDocsMetadata = { site_description: 'test-site-desc', }; +const mockUseParams = jest.fn(); +mockUseParams.mockReturnValue({ '*': 'foo/bar/baz/' }); + +jest.mock('react-router-dom', () => { + return { + ...(jest.requireActual('react-router-dom') as any), + useParams: () => mockUseParams(), + }; +}); + const getEntityMetadata = jest.fn(); const getTechDocsMetadata = jest.fn(); @@ -56,6 +70,18 @@ const techdocsApiMock = { getTechDocsMetadata, }; +const forEntity = jest.fn(); + +forEntity.mockReturnValue({ + snapshot: { + primaryTitle: 'Test Entity', + }, +}); + +const entityPresentationApiMock = { + forEntity, +}; + const Wrapper = ({ entityRef = { kind: mockEntityMetadata.kind, @@ -67,7 +93,12 @@ const Wrapper = ({ entityRef?: CompoundEntityRef; children: React.ReactNode; }) => ( - + {children} @@ -96,9 +127,10 @@ describe('', () => { expect(rendered.getAllByText('test-site-name')).toHaveLength(2); expect(rendered.getByText('test-site-desc')).toBeDefined(); - expect( - rendered.getByRole('link', { name: 'test:test-namespace/test-name' }), - ).toHaveAttribute('href', '/catalog/test-namespace/test/test-name'); + expect(rendered.getByRole('link', { name: 'Test Entity' })).toHaveAttribute( + 'href', + '/catalog/test-namespace/test/test-name', + ); }); it('should render a techdocs page header even if metadata is not loaded', async () => { @@ -152,4 +184,27 @@ describe('', () => { expect(rendered.container.innerHTML).not.toContain('header'); }); + + it('The header title changes depending on the url params', async () => { + getEntityMetadata.mockResolvedValue(mockEntityMetadata); + getTechDocsMetadata.mockResolvedValue(mockTechDocsMetadata); + + await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, + '/docs': rootRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(document.title).toEqual( + 'Backstage | Test Entity | Foo | Bar | Baz', + ); + }); + }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx index ea3cf0f334..3acf4356f5 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx @@ -29,17 +29,23 @@ import { TechDocsMetadata, } from '@backstage/plugin-techdocs-react'; import { + entityPresentationApiRef, EntityRefLink, EntityRefLinks, getEntityRelations, } from '@backstage/plugin-catalog-react'; -import { RELATION_OWNED_BY, CompoundEntityRef } from '@backstage/catalog-model'; +import { + RELATION_OWNED_BY, + CompoundEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { Header, HeaderLabel } from '@backstage/core-components'; import { useRouteRef, configApiRef, useApi } from '@backstage/core-plugin-api'; -import { capitalize } from 'lodash'; +import capitalize from 'lodash/capitalize'; import { rootRouteRef } from '../../../routes'; +import { useParams } from 'react-router-dom'; const skeleton = ; @@ -68,6 +74,9 @@ export const TechDocsReaderPageHeader = ( const addons = useTechDocsAddons(); const configApi = useApi(configApiRef); + const entityPresentationApi = useApi(entityPresentationApiRef); + const { '*': path = '' } = useParams(); + const { title, setTitle, @@ -91,7 +100,6 @@ export const TechDocsReaderPageHeader = ( }, [metadata, setTitle, setSubtitle]); const appTitle = configApi.getOptional('app.title') || 'Backstage'; - const tabTitle = [title, subtitle, appTitle].filter(Boolean).join(' | '); const { locationMetadata, spec } = entityMetadata || {}; const lifecycle = spec?.lifecycle; @@ -157,6 +165,26 @@ export const TechDocsReaderPageHeader = ( 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('/') + .slice(0, 3) + .map(normalizeAndSpace); + + const tabTitleItems = [appTitle, entityDisplayName, ...techdocsTabTitleItems]; + const tabTitle = tabTitleItems.join(' | '); + return (