diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx index 32f9a19a07..2fde91a1f8 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.stories.tsx @@ -15,15 +15,19 @@ */ import React, { ComponentType } from 'react'; -import { List, ListItem } from '@material-ui/core'; import { MemoryRouter } from 'react-router'; +import { List, ListItem } from '@material-ui/core'; + import { Link } from '@backstage/core-components'; import { TestApiProvider } from '@backstage/test-utils'; +import { SearchDocument } from '@backstage/plugin-search-common'; import { searchApiRef, MockSearchApi } from '../../api'; import { SearchContextProvider } from '../../context'; + import { DefaultResultListItem } from '../DefaultResultListItem'; + import { SearchResult } from './SearchResult'; const mockResults = { @@ -55,15 +59,15 @@ const mockResults = { ], }; +const searchApiMock = new MockSearchApi(mockResults); + export default { title: 'Plugins/Search/SearchResult', component: SearchResult, decorators: [ (Story: ComponentType<{}>) => ( - + @@ -73,6 +77,17 @@ export default { ], }; +const CustomResultListItem = (props: { result: SearchDocument }) => { + const { result } = props; + return ( + + + {result.title} - {result.text} + + + ); +}; + export const Default = () => { return ( @@ -82,18 +97,50 @@ export const Default = () => { switch (type) { case 'custom-result-item': return ( - ); default: return ( - - - {document.title} - {document.text} - - + + ); + } + })} + + )} + + ); +}; + +export const WithQuery = () => { + const query = { + term: 'documentation', + }; + + return ( + + {({ results }) => ( + + {results.map(({ type, document }) => { + switch (type) { + case 'custom-result-item': + return ( + + ); + default: + return ( + ); } })} diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index 46a2dde305..a3e4c0e1f0 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -15,69 +15,215 @@ */ import React from 'react'; +import useAsync, { AsyncState } from 'react-use/lib/useAsync'; import { EmptyState, Progress, ResponseErrorPanel, } from '@backstage/core-components'; -import { AnalyticsContext } from '@backstage/core-plugin-api'; -import { SearchResult } from '@backstage/plugin-search-common'; +import { AnalyticsContext, useApi } from '@backstage/core-plugin-api'; +import { SearchQuery, SearchResultSet } from '@backstage/plugin-search-common'; import { useSearch } from '../../context'; +import { searchApiRef } from '../../api'; /** - * Props for {@link SearchResultComponent} - * + * Props for {@link SearchResultContext} * @public */ -export type SearchResultProps = { - children: (results: { results: SearchResult[] }) => JSX.Element; +export type SearchResultContextProps = { + /** + * A child function that receives an asynchronous result set and returns a react element. + */ + children: (state: AsyncState) => JSX.Element; }; /** - * A component returning the search result. - * + * Provides context-based results to a child function. + * @param props - see {@link SearchResultContextProps}. + * @example + * ``` + * + * {({ loading, error, value }) => ( + * + * {value?.map(({ document }) => ( + * + * ))} + * + * )} + * + * ``` * @public */ -export const SearchResultComponent = ({ children }: SearchResultProps) => { - const { - result: { loading, error, value }, - } = useSearch(); - - if (loading) { - return ; - } - if (error) { - return ( - - ); - } - - if (!value?.results.length) { - return ; - } - - return <>{children({ results: value.results })}; +export const SearchResultContext = (props: SearchResultContextProps) => { + const { children } = props; + const context = useSearch(); + const state = context.result; + return children(state); }; /** + * Props for {@link SearchResultApi} * @public */ -const HigherOrderSearchResult = (props: SearchResultProps) => { - return ( - - - +export type SearchResultApiProps = SearchResultContextProps & { + query: Partial; +}; + +/** + * Request results through the search api and provide them to a child function. + * @param props - see {@link SearchResultApiProps}. + * @example + * ``` + * + * {({ loading, error, value }) => ( + * + * {value?.map(({ document }) => ( + * + * ))} + * + * )} + * + * ``` + * @public + */ +export const SearchResultApi = (props: SearchResultApiProps) => { + const { query, children } = props; + const searchApi = useApi(searchApiRef); + + const state = useAsync( + () => + searchApi.query({ + term: query.term ?? '', + types: query.types ?? [], + filters: query.filters ?? {}, + pageCursor: query.pageCursor, + }), + [query], + ); + + return children(state); +}; + +/** + * Props for {@link SearchResultState} + * @public + */ +export type SearchResultStateProps = SearchResultContextProps & + Partial; + +/** + * Call a child render function passing a search state as an argument. + * @remarks By default, results are taken from context, but when a "query" prop is set, results are requested from the search api. + * @param props - see {@link SearchResultStateProps}. + * @example + * Consuming results from context: + * ``` + * + * {({ loading, error, value }) => ( + * + * {value?.map(({ document }) => ( + * + * ))} + * + * )} + * + * ``` + * @example + * Requesting results using the search api: + * ``` + * + * {({ loading, error, value }) => ( + * + * {value?.map(({ document }) => ( + * + * ))} + * + * )} + * + * ``` + * @public + */ +export const SearchResultState = (props: SearchResultStateProps) => { + const { query, children } = props; + + return query ? ( + {children} + ) : ( + {children} ); }; -export { HigherOrderSearchResult as SearchResult }; +/** + * Props for {@link SearchResult} + * @public + */ +export type SearchResultProps = Pick & { + children: (resultSet: SearchResultSet) => JSX.Element; +}; + +/** + * Renders results from a parent search context or api. + * @remarks default components for loading, error and empty variants are returned. + * @param props - see {@link SearchResultProps}. + * @public + */ +export const SearchResultComponent = (props: SearchResultProps) => { + const { query, children } = props; + + return ( + + {({ loading, error, value }) => { + if (loading) { + return ; + } + + if (error) { + return ( + + ); + } + + if (!value?.results.length) { + return ( + + ); + } + + return children(value); + }} + + ); +}; + +/** + * A component returning the search result from a parent search context or api. + * @param props - see {@link SearchResultProps}. + * @public + */ +export const SearchResult = (props: SearchResultProps) => ( + + + +); diff --git a/plugins/search-react/src/components/SearchResult/index.tsx b/plugins/search-react/src/components/SearchResult/index.tsx index 503ac47bbd..eab599883b 100644 --- a/plugins/search-react/src/components/SearchResult/index.tsx +++ b/plugins/search-react/src/components/SearchResult/index.tsx @@ -14,5 +14,17 @@ * limitations under the License. */ -export { SearchResult, SearchResultComponent } from './SearchResult'; -export type { SearchResultProps } from './SearchResult'; +export { + SearchResult, + SearchResultApi, + SearchResultContext, + SearchResultState, + SearchResultComponent, +} from './SearchResult'; + +export type { + SearchResultProps, + SearchResultApiProps, + SearchResultContextProps, + SearchResultStateProps, +} from './SearchResult';