diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1c80d5bac2..cc4d2d26be5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
- Adjusted `EuiDatePickerRange` to allow for deeper customization ([#1219](https://github.com/elastic/eui/pull/1219))
- Added `contentProps` and `textProps` to `EuiButton` and `EuiButtonEmpty` ([#1219](https://github.com/elastic/eui/pull/1219))
- TypeScript types are now published to a `eui.d.ts` top-level file ([#1304](https://github.com/elastic/eui/pull/1304))
+- Added `filterWith` option for `EuiSearchBar` filters of type `field_value_selection` ([#1328](https://github.com/elastic/eui/pull/1328))
**Bug fixes**
@@ -162,7 +163,7 @@
**Bug fixes**
- Fixed an issue in `EuiTooltip` because IE1 didn't support `document.contains()` ([#1190](https://github.com/elastic/eui/pull/1190))
-- Fixed some issues around parsing string values in EuiSearchBar / EuiQuery ([#1189](https://github.com/elastic/eui/pull/1189))
+- Fixed some issues around parsing string values in `EuiSearchBar` and `EuiQuery` ([#1189](https://github.com/elastic/eui/pull/1189))
## [`4.0.0`](https://github.com/elastic/eui/tree/v4.0.0)
diff --git a/src-docs/src/views/search_bar/search_bar_example.js b/src-docs/src/views/search_bar/search_bar_example.js
index 175b42a2176..e407854d189 100644
--- a/src-docs/src/views/search_bar/search_bar_example.js
+++ b/src-docs/src/views/search_bar/search_bar_example.js
@@ -13,6 +13,7 @@ import {
import { SearchBar } from './search_bar';
import { ControlledSearchBar } from './controlled_search_bar';
+import { SearchBarFilters } from './search_bar_filters';
const searchBarSource = require('!!raw-loader!./search_bar');
const searchBarHtml = renderToHtml(SearchBar);
@@ -20,6 +21,9 @@ const searchBarHtml = renderToHtml(SearchBar);
const controlledSearchBarSource = require('!!raw-loader!./controlled_search_bar');
const controlledSearchBarHtml = renderToHtml(ControlledSearchBar);
+const searchBarFiltersSource = require('!!raw-loader!./search_bar_filters');
+const searchBarFiltersHtml = renderToHtml(SearchBarFilters);
+
export const SearchBarExample = {
title: 'Search Bar',
sections: [
@@ -116,6 +120,26 @@ export const SearchBarExample = {
),
demo:
+ },
+ {
+ title: 'Search Bar Filters',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: searchBarFiltersSource,
+ }, {
+ type: GuideSectionTypes.HTML,
+ code: searchBarFiltersHtml,
+ }
+ ],
+ text: (
+
+
+ A EuiSearchBar can have custom filter drop downs that control how a user can search.
+
+
+ ),
+ demo:
}
],
};
diff --git a/src-docs/src/views/search_bar/search_bar_filters.js b/src-docs/src/views/search_bar/search_bar_filters.js
new file mode 100644
index 00000000000..086bef43785
--- /dev/null
+++ b/src-docs/src/views/search_bar/search_bar_filters.js
@@ -0,0 +1,300 @@
+import React, { Component, Fragment } from 'react';
+import { times } from 'lodash';
+import { Random } from '../../../../src/services/random';
+import {
+ EuiHealth,
+ EuiCallOut,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiCodeBlock,
+ EuiTitle,
+ EuiBasicTable,
+ EuiSearchBar,
+} from '../../../../src/components';
+
+const random = new Random();
+
+const tags = [
+ { name: 'marketing', color: 'danger' },
+ { name: 'finance', color: 'success' },
+ { name: 'eng', color: 'success' },
+ { name: 'sales', color: 'warning' },
+ { name: 'ga', color: 'success' },
+ { name: 'presales', color: 'success' },
+ { name: 'product', color: 'warning' },
+ { name: 'engineering', color: 'success' },
+ { name: 'design', color: 'warning' },
+ { name: 'earlybirds', color: 'success' },
+ { name: 'people-ops', color: 'danger' },
+ { name: 'solutions', color: 'success' },
+ { name: 'elasticsearch', color: 'success' },
+ { name: 'kibana', color: 'success' },
+ { name: 'cloud', color: 'danger' },
+ { name: 'logstash', color: 'warning' },
+ { name: 'beats', color: 'warning' },
+ { name: 'legal', color: 'danger' },
+ { name: 'revenue', color: 'success' },
+ { name: 'public-relations', color: 'success' },
+ { name: 'social-media-management', color: 'warning' },
+];
+
+const types = [
+ 'dashboard',
+ 'visualization',
+ 'watch',
+];
+
+const users = [
+ 'dewey',
+ 'wanda',
+ 'carrie',
+ 'jmack',
+ 'gabic',
+];
+
+const items = times(10, (id) => {
+ return {
+ id,
+ status: random.oneOf(['open', 'closed']),
+ type: random.oneOf(types),
+ tag: random.setOf(tags.map(tag => tag.name), { min: 0, max: 3 }),
+ active: random.boolean(),
+ owner: random.oneOf(users),
+ followers: random.integer({ min: 0, max: 20 }),
+ comments: random.integer({ min: 0, max: 10 }),
+ stars: random.integer({ min: 0, max: 5 })
+ };
+});
+
+const initialQuery = EuiSearchBar.Query.MATCH_ALL;
+
+export class SearchBarFilters extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ query: initialQuery,
+ result: items,
+ error: null
+ };
+ }
+
+ onChange = ({ query, error }) => {
+ if (error) {
+ this.setState({ error });
+ } else {
+ this.setState({
+ error: null,
+ result: EuiSearchBar.Query.execute(query, items, { defaultFields: ['owner', 'tag', 'type'] }),
+ query
+ });
+ }
+ };
+
+ renderSearch() {
+ const filters = [
+ {
+ type: 'field_value_selection',
+ field: 'tag',
+ name: 'Tag ("prefix" filter, default)',
+ multiSelect: 'or',
+ options: tags.map(tag => ({
+ value: tag.name,
+ view: {tag.name}
+ }))
+ },
+ {
+ type: 'field_value_selection',
+ field: 'tag',
+ name: 'Tag ("includes" filter)',
+ filterWith: 'includes',
+ multiSelect: 'or',
+ options: tags.map(tag => ({
+ value: tag.name,
+ view: {tag.name}
+ }))
+ },
+ {
+ type: 'field_value_selection',
+ field: 'tag',
+ name: 'Tag (custom filter)',
+ filterWith: () => Math.random() > 0.5,
+ multiSelect: 'or',
+ options: tags.map(tag => ({
+ value: tag.name,
+ view: {tag.name}
+ }))
+ }
+ ];
+
+ const schema = {
+ strict: true,
+ fields: {
+ active: {
+ type: 'boolean'
+ },
+ status: {
+ type: 'string'
+ },
+ followers: {
+ type: 'number'
+ },
+ comments: {
+ type: 'number'
+ },
+ stars: {
+ type: 'number'
+ },
+ created: {
+ type: 'date'
+ },
+ owner: {
+ type: 'string'
+ },
+ tag: {
+ type: 'string',
+ validate: (value) => {
+ if (!tags.some(tag => tag.name === value)) {
+ throw new Error(`unknown tag (possible values: ${tags.map(tag => tag.name).join(',')})`);
+ }
+ }
+ }
+ }
+ };
+
+ return (
+
+ );
+ }
+
+ renderError() {
+ const { error } = this.state;
+ if (!error) {
+ return;
+ }
+ return (
+
+
+
+
+ );
+ }
+
+ renderTable() {
+ const columns = [
+ {
+ name: 'Type',
+ field: 'type'
+ },
+ {
+ name: 'Open',
+ field: 'status',
+ render: (status) => status === 'open' ? 'Yes' : 'No'
+ },
+ {
+ name: 'Active',
+ field: 'active',
+ dataType: 'boolean'
+ },
+ {
+ name: 'Tags',
+ field: 'tag'
+ },
+ {
+ name: 'Owner',
+ field: 'owner'
+ },
+ {
+ name: 'Stats',
+ width: '150px',
+ render: (item) => {
+ return (
+
+
{`${item.stars} Stars`}
+
{`${item.followers} Followers`}
+
{`${item.comments} Comments`}
+
+ );
+ }
+ }
+ ];
+
+ const queriedItems = EuiSearchBar.Query.execute(this.state.query, items, {
+ defaultFields: ['owner', 'tag', 'type']
+ });
+
+ return (
+
+ );
+ }
+
+ render() {
+ const {
+ query,
+ } = this.state;
+
+ const esQueryDsl = EuiSearchBar.Query.toESQuery(query);
+ const esQueryString = EuiSearchBar.Query.toESQueryString(query);
+
+ const content = this.renderError() || (
+
+
+
+
+ Elasticsearch Query String
+
+
+
+ {esQueryString ? esQueryString : ''}
+
+
+
+
+
+ Elasticsearch Query DSL
+
+
+
+ {esQueryDsl ? JSON.stringify(esQueryDsl, null, 2) : ''}
+
+
+
+
+
+
+ JS execution
+
+
+
+
+ {this.renderTable()}
+
+
+ );
+
+ return (
+
+ {this.renderSearch()}
+
+ {content}
+
+ );
+ }
+}
diff --git a/src/components/search_bar/filters/field_value_selection_filter.js b/src/components/search_bar/filters/field_value_selection_filter.js
index b206fbfcc7d..0628541d017 100644
--- a/src/components/search_bar/filters/field_value_selection_filter.js
+++ b/src/components/search_bar/filters/field_value_selection_filter.js
@@ -30,6 +30,7 @@ export const FieldValueSelectionFilterConfigType = PropTypes.shape({
autoClose: PropTypes.boolean,
name: PropTypes.string.isRequired,
options: FieldValueOptionsType.isRequired,
+ filterWith: PropTypes.oneOfType([ PropTypes.func, PropTypes.oneOf([ 'prefix', 'includes' ]) ]),
cache: PropTypes.number,
multiSelect: PropTypes.oneOfType([ PropTypes.bool, PropTypes.oneOf([ 'and', 'or' ]) ]),
loadingMessage: PropTypes.string,
@@ -48,6 +49,7 @@ const FieldValueSelectionFilterPropTypes = {
const defaults = {
config: {
multiSelect: true,
+ filterWith: 'prefix',
loadingMessage: 'Loading...',
noOptionsMessage: 'No options found',
searchThreshold: 10,
@@ -108,23 +110,41 @@ export class FieldValueSelectionFilter extends Component {
});
}
- filterOptions(prefix = '') {
+ filterOptions(q = '') {
this.setState(prevState => {
if (isNil(prevState.options)) {
return {};
}
+
+ const predicate = this.getOptionFilter();
+
return {
options: {
...prevState.options,
- shown: prevState.options.all.filter(option => {
- const name = this.resolveOptionName(option);
- return name.toLowerCase().startsWith(prefix.toLowerCase());
+ shown: prevState.options.all.filter((option, i, options) => {
+ const name = this.resolveOptionName(option).toLowerCase();
+ const query = q.toLowerCase();
+ return predicate(name, query, options);
})
}
};
});
}
+ getOptionFilter() {
+ const filterWith = this.props.config.filterWith || defaults.config.filterWith;
+
+ if (typeof filterWith === 'function') {
+ return filterWith;
+ }
+
+ if (filterWith === 'includes') {
+ return (name, query) => name.includes(query);
+ }
+
+ return (name, query) => name.startsWith(query);
+ }
+
resolveOptionsLoader() {
const options = this.props.config.options;
if (isArray(options)) {