ra-search
Plug your search engine and let users search across all resources via a smart Omnibox.
In large admins, users need several clicks to get to one record. For repetitive tasks, this ends up costing minutes every day. The ra-seach omnibox simplifies navigation by providing a global, always on search engine for records.
Ra-search can take advantage of a search index like ElasticSearch if you have one, or it can rely on your REST API by searching across multiple resources in parallel.
Installation
npm install --save @react-admin/ra-search
# or
yarn add @react-admin/ra-search
Tip: ra-search
is part of the React-Admin Enterprise Edition, and hosted in a private npm registry. You need to subscribe to one of the Enterprise Edition plans to access this package.
dataProvider
Configuring the dataProvider.search()
ra-search
passes search queries to the dataProvider, which must contain a custom search
method with the following signature:
type search = (query: string, options?: SearchOptions) => Promise<{ data: SearchResult[], total: number }>
interface SearchOptions {
targets?: string[];
[key: string]: any;
}
interface SearchResult {
id: Identifier;
type: string;
url: string;
content: any;
matches?: any;
}
Here is an example of the expected syntax for dataProvider.search()
:
dataProvider.search('roll').then(response => console.log(response));
// {
// data: [
// { id: 'a7535', type: 'artist', url: '/artists/7535', content: { label: 'The Rolling Stones', description: 'English rock band formed in London in 1962' } }
// { id: 'a5352', type: 'artist', url: '/artists/5352', content: { label: 'Sonny Rollins', description: 'American jazz tenor saxophonist' } }
// { id: 't7524', type: 'track', url: '/tracks/7524', content: { label: 'Like a Rolling Stone', year: 1965, recordCompany: 'Columbia', artistId: 345, albumId: 435456 } }
// { id: 't2386', type: 'track', url: '/tracks/2386', content: { label: "It's Only Rock 'N Roll (But I Like It)", year: 1974, artistId: 7535, albumId: 6325 } }
// { id: 'a6325', type: 'album', url: '/albums/6325', content: { label: "It's Only rock 'N Roll", year: 1974, artistId: 7535 }}
// ],
// total: 5
// }
It is your responsibility to add this search
method to your dataProvider
so that react-admin can send queries to and read responses from the search engine.
The dataProvider.search()
method should return a Promise for data containing an array of SearchResult
objects. A SearchResult
contains at least the following fields:
id
:Identifier
The unique identifier of the search resulttype
:string
An arbitrary string which enables groupingurl
:string
The url where to redirect to on click. It could be a custom page and not a resource if you want tocontent
:any
Can contains any data that will be used to display the result. If used with default<SearchResultItem>
component, it must contain at least anid
,label
and adescription
.matches
:any
An optional object containing an extract of the data with matches. Can be anything that will be interpreted by a<SearchResultItem>
As for the total
, it can be grater than the number of returned results. This is useful e.g. to show that there are more results.
addSearchMethod
Helper
If you don't have a full-text search endpoint in your API, you can use the simple addSearchMethod
helper function. It adds the search()
method to an existing dataProvider
, reyling on the dataProvider.getList()
method on the configured resources.
import simpleRestProvider from 'ra-data-simple-rest';
import { addSearchMethod } from '@react-admin/ra-search';
const dataProvider = simpleRestProvider('http://path.to.my.api/');
const dataProviderWithSearch = addSearchMethod(dataProvider, ['artists', 'tracks', 'albums']);
Now calling dataProvider.search('roll')
will issue the following queries in parallel:
dataProvider.getList('artists', { filter: { q: "roll" }})
dataProvider.getList('tracks', { filter: { q: "roll" }})
dataProvider.getList('albumns', { filter: { q: "roll" }})
Then aggregate the results and return them in a single response.
So the search()
method created by this helper calls the regular dataProvider
several times, once for each resource. We don't recommend using it in production - instead, you should modify your API to support the search method, e.g. by using a search engine, and implement your own dataProvider.search()
method to convert the results to the format expected by ra-search
.
The second argument to addSearchMethod
is the builder configuraiton. It can be either an array of resources names or a map of the resources specifying how to format their records for search results.
When called with an array of resources, addSearchMethod
populates the search results content
based on the records returned by dataProvider.getList()
, with the following inference rules:
id
: Returns the recordid
label
: Returns the recordlabel
orname
ortitle
description
: Returns the recorddescription
orbody
Example with an array of resources:
const dataProviderWithSearch = addSearchMethod(dataProvider, ['artists', 'albums']);
When called with a map, each key being a resource name, the value can have the following properties:
label
: Either the field name to use as the label or a function which will be called with a record and must return a string. Defaults to the inference described above.description
: Either the field name to use as the description or a function which will be called with a record and must return a string. Defaults to the inference described above.
Example with a map of resources:
const dataProviderWithSearch = addSearchMethod(dataProvider, {
artists: {
label: 'full_name',
description: record => `${record.born_at}-${record.died_at} ${record.biography}`,
},
albums: {
// no label specified, fallback on inference
description: record => `${record.released_at.getFullYear()} by ${record.recordCompany}`,
},
});
<Search>
Component
The The <Search>
component includes an input and displays the search results inside a Material-UI PopOver
.
By default, it will group the search results by target, and show their content.label
and content.description
.
Here's how to include it inside a custom <AppBar>
component:
import { Admin, AppBar, Layout, Resource } from 'react-admin';
import { Typography, makeStyles } from '@material-ui/core';
import { Search } from '@react-admin/ra-search';
const MyAppbar = (props) => {
const classes = useStyles();
return (
<AppBar {...props}>
<Typography
variant="h6"
color="inherit"
className={classes.title}
id="react-admin-title"
/>
<Search />
</AppBar>
);
};
const useStyles = makeStyles({
title: {
flex: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
},
});
const MyLayout = (props) => (
<Layout {...props} appBar={MyAppbar} />
);
export const App = () => (
<Admin dataProvider={searchDataProvider} layout={MyLayout}>
{...}
</Admin>
);
The <Search>
component accepts the following props:
targets
:string[]
An array of the indices on which to perform the search. Defaults to an empty array.children
: A component which will display the results inside the<Popover>
. Defaults to<SearchResultsPanel>
.color
: The color mode for the input, applying light or dark backgrounds. Accept eitherlight
ordark
. Defaults tolight
.
Customizing The Result Items
By default, <Search>
displays the results in <SearchResultsPanel>
, which displays each result in a <SearchResultItem>
. So rendering <Search />
without children is equivalent to rendering:
const MySearch = () => (
<Search>
<SearchResultsPanel>
<SearchResultItem/>
</SearchResultsPanel>
</Search>
);
<SearchResultItem>
renders the content.label
and content.description
for each result. You can customize what it renders by providing a function as the label
and a the description
props. This function takes the search result as parameter and must return a React element.
For instance:
import { Search, SearchResultsPanel, SearchResultItem } from '@react-admin/ra-search';
const MySearch = () => (
<Search>
<SearchResultsPanel>
<SearchResultItem
label={(record) => (
<>
{record.type === 'artists' ? (
<PersonIcon />
) : (
<MusicIcon />
)}
<span>{record.content.label}</span>
</>
)}
/>
</SearchResultsPanel>
</Search>
);
You can also completely replace the search result item component:
import { Search, SearchResultsPanel } from '@react-admin/ra-search';
const MySearchResultItem = ({ data, onClose }) => (
<li key={data.id}>
<Link to={data.url} onClick={onClose}>
<strong>{data.content.label}</strong>
</Link>
<p>{data.content.description}</p>
</li>
);
const MySearch = () => (
<Search>
<SearchResultsPanel>
<MySearchResultItem />
</SearchResultsPanel>
</Search>
);
Customizing the Entire Search Results
Pass a custom React element as child of <Search>
to customize the appearance of the search results. This can be useful e.g. to customize the results grouping, or to arrange search results in a different way.
ra-search
renders the <Search>
inside a SearchContext
. You use the useSearchResultContext
hook to read the search results, as follows:
import { Search, useSearchResultContext } from '@react-admin/ra-search';
const MySearch = (props) => (
<Search>
<CustomSearchResultsPanel />
</Search>
);
const CustomSearchResultsPanel = () => {
const { data, onClose } = useSearchResultContext();
return (
<ul>
{data.map(searchResult => (
<li key={searchResult.id}>
<Link to={searchResult.url} onClick={onClose}>
<strong>{searchResult.content.label}</strong>
</Link>
<p>{searchResult.content.description}</p>
</li>
))}
</ul>
);
};
useSearch
Hook
The Just like useMutation
, useSearch
returns a function allowing to call the dataProvider.search()
, as well as a state object for the response. Use it to create your own Search
component.
import { useState } from 'React';
import { TextField } from '@material-ui/core';
import { useSearch, SearchResultContextProvider, SearchResultsPanel } from '@react-admin/ra-search';
const Search = () => {
const [query, setQuery] = useState();
const [search, searchResultState] = useSearch();
const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
setQuery(event.target.value);
search(event.target.value);
};
return (
<>
<TextField value={query} onChange={handleChange} />
<SearchResultContextProvider value={searchResultState}>
<SearchResultsPanel />
</SearchResultContextProvider>
</>
);
};
Navigate in search result with arrow keys
Thanks to the useArrowKeysToNavigate
hook, you can navigate in search results with arrow key. If you want to reimplement it, you have to pass the list ref to the hook and each results must have a button role:
import { List, ListItem } from '@material-ui/core';
const SearchResults = () => {
const listRef = React.useRef<HTMLUListElement>(null);
useArrowKeysToNavigate(listRef);
<List innerRef={listRef}>
{data.map(resultData => {
return <ListItem button data={resultData} key={resultData.id} />
})}
</List>
}
CHANGELOG
v2.1.0
2021-04-22
- (feat) Allows users to navigate in results with up and down arrow keys
v2.0.1
2020-12-18
- (fix) Fix search results panel markup and accessibility
v2.0.0
2020-12-17
- (feat) rename search method options (facets -> targets)
v1.2.3
2020-12-08
- (fix) Fix SearchInput value is not selectable while search results are displayed
v1.2.2
2020-12-03
- (fix) Fix part of terms are sometimes discarded when response is slow
- (fix) Fix empty popover is displayed when there are no more results
v1.2.1
2020-11-23
- (fix) Fix search results popover closes and reopens at each keystroke
v1.2.0
2020-11-04
- (feat) Export groupSearchResultsByResource function
v1.1.1
2020-10-19
- (fix) Display the
<PopOver>
position at the bottom right of the<SearchInput>
- (fix) Remove the stickyness of facet headers
- (feat) Add a growing effect when focusing the menu
v1.1.0
2020-10-14
- (fix) Hide clear button when there is no value in the
<SearchInput>
- (feat) Add
color
prop to both<SearchInput>
and<Search>
component supportinglight
anddark
v1.0.0
2020-10-13
- First release