diff --git a/.changeset/olive-berries-jump.md b/.changeset/olive-berries-jump.md new file mode 100644 index 0000000000..d8e9fb1da6 --- /dev/null +++ b/.changeset/olive-berries-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Added support for the Search bar in docs residing in the entity page tab, and not only the global "/docs" page. diff --git a/plugins/techdocs/report.api.md b/plugins/techdocs/report.api.md index c58a946287..2cc3934f62 100644 --- a/plugins/techdocs/report.api.md +++ b/plugins/techdocs/report.api.md @@ -371,6 +371,7 @@ export const TechDocsReaderPageContent: ( export type TechDocsReaderPageContentProps = { entityRef?: CompoundEntityRef; withSearch?: boolean; + searchResultUrlMapper?: (url: string) => string; onReady?: () => void; }; @@ -431,6 +432,7 @@ export type TechDocsSearchProps = { entityId: CompoundEntityRef; entityTitle?: string; debounceTime?: number; + searchResultUrlMapper?: (url: string) => string; }; // @public diff --git a/plugins/techdocs/src/EntityPageDocs.tsx b/plugins/techdocs/src/EntityPageDocs.tsx index 07222190ae..70a0517ded 100644 --- a/plugins/techdocs/src/EntityPageDocs.tsx +++ b/plugins/techdocs/src/EntityPageDocs.tsx @@ -25,12 +25,24 @@ import React from 'react'; import { TechDocsReaderPage } from './plugin'; import { TechDocsReaderPageContent } from './reader/components/TechDocsReaderPageContent'; import { TechDocsReaderPageSubheader } from './reader/components/TechDocsReaderPageSubheader'; +import { useEntityPageTechDocsRedirect } from './search/hooks/useTechDocsLocation'; -type EntityPageDocsProps = { entity: Entity }; +type EntityPageDocsProps = { + entity: Entity; + /** + * Show or hide the content search bar, defaults to true. + */ + withSearch?: boolean; +}; -export const EntityPageDocs = ({ entity }: EntityPageDocsProps) => { +export const EntityPageDocs = ({ + entity, + withSearch = true, +}: EntityPageDocsProps) => { let entityRef = getCompoundEntityRef(entity); + const searchResultUrlMapper = useEntityPageTechDocsRedirect(entityRef); + if (entity.metadata.annotations?.[TECHDOCS_EXTERNAL_ANNOTATION]) { try { entityRef = parseEntityRef( @@ -44,7 +56,10 @@ export const EntityPageDocs = ({ entity }: EntityPageDocsProps) => { return ( - + ); }; diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx index 21418a716e..f8b3129feb 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/TechDocsReaderPageContent.tsx @@ -64,6 +64,13 @@ export type TechDocsReaderPageContentProps = { * Show or hide the search bar, defaults to true. */ withSearch?: boolean; + /** + * If {@link TechDocsReaderPageContentProps.withSearch | withSearch} is true, + * this will redirect the search result urls, e.g. turn search results into + * links within the "Docs" tab of the entity page, instead of the global docs + * page. + */ + searchResultUrlMapper?: (url: string) => string; /** * Callback called when the content is rendered. */ @@ -76,7 +83,7 @@ export type TechDocsReaderPageContentProps = { */ export const TechDocsReaderPageContent = withTechDocsReaderProvider( (props: TechDocsReaderPageContentProps) => { - const { withSearch = true, onReady } = props; + const { withSearch = true, searchResultUrlMapper, onReady } = props; const classes = useStyles(); const { @@ -142,6 +149,7 @@ export const TechDocsReaderPageContent = withTechDocsReaderProvider( )} diff --git a/plugins/techdocs/src/search/components/TechDocsSearch.tsx b/plugins/techdocs/src/search/components/TechDocsSearch.tsx index ce77d547db..b283581fa2 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearch.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearch.tsx @@ -34,6 +34,7 @@ export type TechDocsSearchProps = { entityId: CompoundEntityRef; entityTitle?: string; debounceTime?: number; + searchResultUrlMapper?: (url: string) => string; }; type TechDocsDoc = { @@ -58,7 +59,12 @@ const isTechDocsSearchResult = ( }; const TechDocsSearchBar = (props: TechDocsSearchProps) => { - const { entityId, entityTitle, debounceTime = 150 } = props; + const { + entityId, + entityTitle, + debounceTime = 150, + searchResultUrlMapper, + } = props; const [open, setOpen] = useState(false); const navigate = useNavigate(); const { @@ -102,7 +108,9 @@ const TechDocsSearchBar = (props: TechDocsSearchProps) => { ) => { if (isTechDocsSearchResult(selection)) { const { location } = selection.document; - navigate(location); + navigate( + searchResultUrlMapper ? searchResultUrlMapper(location) : location, + ); } }; diff --git a/plugins/techdocs/src/search/hooks/useTechDocsLocation.ts b/plugins/techdocs/src/search/hooks/useTechDocsLocation.ts new file mode 100644 index 0000000000..737dd2839c --- /dev/null +++ b/plugins/techdocs/src/search/hooks/useTechDocsLocation.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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 { useMemo } from 'react'; + +import { CompoundEntityRef } from '@backstage/catalog-model'; +import { useRouteRef } from '@backstage/core-plugin-api'; + +import { rootCatalogDocsRouteRef, rootDocsRouteRef } from '../../routes'; + +const trimStartSlash = (path: string) => path.replace(/^\/+/, ''); +const trimEndSlash = (path: string) => path.replace(/\/+$/, ''); + +/** + * Returns a function that takes a location to a Tech Docs entry, and returns a + * new location, re-routed to the catalog page tab. + * + * @internal + */ +export function useEntityPageTechDocsRedirect(entityRef: CompoundEntityRef) { + const { kind, name, namespace } = entityRef; + + const routeDocsRoot = useRouteRef(rootDocsRouteRef); + const routeDocsCatalog = useRouteRef(rootCatalogDocsRouteRef); + + // Re-routes a /docs/:namespace/:kind/:name/* location into + // /catalog/:namespace/:kind/:name/docs/*, while handling situations where + // these defaults are changed. + // eslint-disable-next-line react-hooks/rules-of-hooks + const reRouteLocationToCatalog = useMemo(() => { + const rootDocsPath = trimEndSlash(routeDocsRoot({ kind, namespace, name })); + const catalogDocsPath = trimEndSlash(routeDocsCatalog()); + + return (url: string): string => { + if ( + url + .toLocaleLowerCase('en-US') + .startsWith(rootDocsPath.toLocaleLowerCase('en-US')) + ) { + const suffix = trimStartSlash(url.slice(rootDocsPath.length)); + return suffix.length === 0 || suffix.startsWith('#') + ? `${catalogDocsPath}${suffix}` + : `${catalogDocsPath}/${suffix}`; + } + return url; + }; + }, [routeDocsRoot, routeDocsCatalog, kind, name, namespace]); + + return reRouteLocationToCatalog; +}