From 0b159a81f77db0a3dbd0c3937862aba2e0d7f8c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Bevacqua?= Date: Tue, 27 Nov 2018 19:10:58 -0300 Subject: [PATCH] filterWith option for field_value_selection filters (#1328) * filterWith option for field_value_selection filters * Add changelog entry * single quotes * Custom filters example --- CHANGELOG.md | 3 +- .../views/search_bar/search_bar_example.js | 24 ++ .../views/search_bar/search_bar_filters.js | 300 ++++++++++++++++++ .../filters/field_value_selection_filter.js | 28 +- 4 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 src-docs/src/views/search_bar/search_bar_filters.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5933b7bcc27..0c0f21ce3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,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** @@ -164,7 +165,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)) {