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:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+9
-1
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user