From 99f557f2563a9a6fc5019b3971dd904a1ff6bfbb Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Wed, 27 Sep 2023 09:43:12 +0200 Subject: [PATCH] feat(search): create search modal extension Signed-off-by: Camila Belo --- plugins/search/src/alpha.tsx | 264 ++++++++++++++++++++++++++++++++++- 1 file changed, 257 insertions(+), 7 deletions(-) diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index 599997495e..c8a96a4a5a 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -14,10 +14,24 @@ * limitations under the License. */ -import React from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; -import { Grid, makeStyles, Paper, Theme } from '@material-ui/core'; +import { + makeStyles, + Theme, + Grid, + Box, + Paper, + DialogActions, + DialogContent, + DialogTitle, + Button, + IconButton, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; import SearchIcon from '@material-ui/icons/Search'; +import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; import { CatalogIcon, @@ -41,6 +55,8 @@ import { createPageExtension, createExtensionInput, createNavItemExtension, + createExtensionDataRef, + createExtension, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; @@ -49,7 +65,6 @@ import { CATALOG_FILTER_EXISTS, } from '@backstage/plugin-catalog-react'; -import { SearchType } from '@backstage/plugin-search'; import { DefaultResultListItem, SearchBar, @@ -64,9 +79,11 @@ import { SearchResult } from '@backstage/plugin-search-common'; import { searchApiRef } from '@backstage/plugin-search-react'; import { searchResultItemExtensionData } from '@backstage/plugin-search-react/alpha'; -import { UrlUpdater } from './components/SearchPage/SearchPage'; - import { SearchClient } from './apis'; +import { SearchType } from './components/SearchType'; +import { UrlUpdater } from './components/SearchPage/SearchPage'; +import { SidebarSearchModal } from './components/SidebarSearchModal/SidebarSearchModal'; +import { useSearchModal } from './components/SearchModal'; /** @alpha */ export const SearchApi = createApiExtension({ @@ -83,7 +100,7 @@ export const SearchApi = createApiExtension({ }, }); -const useStyles = makeStyles((theme: Theme) => ({ +const useSearchPageStyles = makeStyles((theme: Theme) => ({ filter: { '& + &': { marginTop: theme.spacing(2.5), @@ -119,7 +136,7 @@ export const SearchPage = createPageExtension({ }; const Component = () => { - const classes = useStyles(); + const classes = useSearchPageStyles(); const { isMobile } = useSidebarPinState(); const { types } = useSearch(); const catalogApi = useApi(catalogApiRef); @@ -230,10 +247,243 @@ export const SearchPage = createPageExtension({ }, }); +const searchModalExtensionData = createExtensionDataRef( + 'plugin.search.modal.element', +); + +const useSearchModalStyles = makeStyles(theme => ({ + dialogTitle: { + gap: theme.spacing(1), + display: 'grid', + alignItems: 'center', + gridTemplateColumns: '1fr auto', + '&> button': { + marginTop: theme.spacing(1), + }, + }, + container: { + borderRadius: 30, + display: 'flex', + height: '2.4em', + padding: theme.spacing(1), + }, + filter: { + '& + &': { + marginTop: theme.spacing(2.5), + }, + }, + filters: { + padding: theme.spacing(2), + marginTop: theme.spacing(2), + }, + input: { + flex: 1, + }, + button: { + '&:hover': { + background: 'none', + }, + }, + dialogActionsContainer: { padding: theme.spacing(1, 3) }, + viewResultsLink: { verticalAlign: '0.5em' }, +})); + +/** @alpha */ +export const SearchModal = createExtension({ + id: 'plugin.search.modal', + at: 'plugin.search.nav.index/modal', + configSchema: createSchemaFromZod(z => + z.object({ + noTrack: z.boolean().default(false), + }), + ), + inputs: { + items: createExtensionInput({ + item: searchResultItemExtensionData, + }), + }, + output: { + element: searchModalExtensionData, + }, + factory({ bind, config, inputs }) { + const getResultItemComponent = (result: SearchResult) => { + const value = inputs.items.find(({ item }) => item?.predicate?.(result)); + return value?.item.component ?? DefaultResultListItem; + }; + + const Component = () => { + const classes = useSearchModalStyles(); + const navigate = useNavigate(); + const catalogApi = useApi(catalogApiRef); + + const { types } = useSearch(); + const { toggleModal } = useSearchModal(); + const searchRootRoute = '/search'; + const searchBarRef = useRef(null); + + useEffect(() => { + searchBarRef?.current?.focus(); + }); + + // This handler is called when "enter" is pressed + const handleSearchBarSubmit = useCallback(() => { + toggleModal(); + // Using ref to get the current field value without waiting for a query debounce + const query = searchBarRef.current?.value ?? ''; + navigate(`${searchRootRoute}?query=${query}`); + }, [navigate, toggleModal, searchRootRoute]); + + return ( + <> + + + + + + + + + + + + + + + + {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; + }} + /> + + )} + + + + + + + + + + + + + + + {({ results }) => + results.map((result, index) => { + const SearchResultListItem = + getResultItemComponent(result); + return ( + + ); + }) + } + + + + + + + + + + + + + ); + }; + + bind({ + element: , + }); + }, +}); + /** @alpha */ export const SearchNavItem = createNavItemExtension({ id: 'plugin.search.nav.index', routeRef: searchRouteRef, title: 'Search', icon: SearchIcon, + group: async ({ inputs }) => ( + {() => inputs.modal.element} + ), + inputs: { + modal: createExtensionInput( + { + element: searchModalExtensionData, + }, + { + singleton: true, + }, + ), + }, });