Add Search bar to TechDocs when displayed in the entity tab (#26706)

* feat: Add Search bar to TechDocs when displayed in the entity tab

Closes #23004

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
---------

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
Gustaf Räntilä
2024-11-20 17:06:27 +01:00
committed by GitHub
parent 04362c14fe
commit 7d8777dbba
6 changed files with 106 additions and 6 deletions
+5
View File
@@ -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.
+2
View File
@@ -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
+18 -3
View File
@@ -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 (
<TechDocsReaderPage entityRef={entityRef}>
<TechDocsReaderPageSubheader />
<TechDocsReaderPageContent withSearch={false} />
<TechDocsReaderPageContent
withSearch={withSearch}
searchResultUrlMapper={searchResultUrlMapper}
/>
</TechDocsReaderPage>
);
};
@@ -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(
<TechDocsSearch
entityId={entityRef}
entityTitle={entityMetadata?.metadata?.title}
searchResultUrlMapper={searchResultUrlMapper}
/>
</Grid>
)}
@@ -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,
);
}
};
@@ -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;
}