From a61269e8d45268816e0ad13723b7a3c71d5bf4f1 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 6 Nov 2018 16:18:28 -0500 Subject: [PATCH 01/96] basic groundwork for filter bar euification --- .../kibana/public/discover/index.html | 1 + src/ui/public/filter_bar/index.js | 1 + src/ui/public/filter_bar/react/directive.js | 32 +++++++++++++++++++ src/ui/public/filter_bar/react/filter_bar.tsx | 26 +++++++++++++++ src/ui/public/filter_bar/react/index.ts | 22 +++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 src/ui/public/filter_bar/react/directive.js create mode 100644 src/ui/public/filter_bar/react/filter_bar.tsx create mode 100644 src/ui/public/filter_bar/react/index.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index 1534ca0e580fea..393836bc259b86 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -40,6 +40,7 @@

+ { + return reactDirective( + FilterBar, + undefined, + {}, + ); +}); diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx new file mode 100644 index 00000000000000..03f5e23de76979 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component } from 'react'; + +export class FilterBar extends Component { + public render() { + return
Hello world
; + } +} diff --git a/src/ui/public/filter_bar/react/index.ts b/src/ui/public/filter_bar/react/index.ts new file mode 100644 index 00000000000000..cdf49a72e9554e --- /dev/null +++ b/src/ui/public/filter_bar/react/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './directive'; + +export { FilterBar } from './filter_bar'; From 9c293bf913e1cad09abbf94f62794b6eef9f2d37 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 6 Nov 2018 16:31:51 -0500 Subject: [PATCH 02/96] filter item placeholder --- src/ui/public/filter_bar/react/filter_bar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 03f5e23de76979..563fb8884b6f61 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -18,9 +18,10 @@ */ import React, { Component } from 'react'; +import { FilterItem } from 'ui/filter_bar/react/filter_item'; export class FilterBar extends Component { public render() { - return
Hello world
; + return ; } } From 812407ab6c547261eea2c2e792cd61775966d589 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 6 Nov 2018 16:32:18 -0500 Subject: [PATCH 03/96] filter item placeholder file I missed --- .../public/filter_bar/react/filter_item.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/ui/public/filter_bar/react/filter_item.tsx diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx new file mode 100644 index 00000000000000..5e0369e37255af --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component } from 'react'; + +export class FilterItem extends Component { + public render() { + return
Hello world
; + } +} From 72557850e770caf54511769a1ba739988c22e678 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 6 Nov 2018 17:04:15 -0500 Subject: [PATCH 04/96] create initial filter class --- src/ui/public/filter_bar/filters/index.ts | 25 +++++++++ .../filter_bar/filters/phrase_filter.ts | 52 +++++++++++++++++++ src/ui/public/filter_bar/react/filter_bar.tsx | 5 +- .../public/filter_bar/react/filter_item.tsx | 13 +++-- 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/ui/public/filter_bar/filters/index.ts create mode 100644 src/ui/public/filter_bar/filters/phrase_filter.ts diff --git a/src/ui/public/filter_bar/filters/index.ts b/src/ui/public/filter_bar/filters/index.ts new file mode 100644 index 00000000000000..e317ef5827db23 --- /dev/null +++ b/src/ui/public/filter_bar/filters/index.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface Filter { + disabled: boolean; + negate: boolean; + index: string; + getDisplayText: () => string; +} diff --git a/src/ui/public/filter_bar/filters/phrase_filter.ts b/src/ui/public/filter_bar/filters/phrase_filter.ts new file mode 100644 index 00000000000000..e123bb21ecc5ff --- /dev/null +++ b/src/ui/public/filter_bar/filters/phrase_filter.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filter } from 'ui/filter_bar/filters/index'; + +export class PhraseFilter implements Filter { + public field: string; + public value: string | number; + public disabled: boolean; + public index: string; + public negate: boolean; + + constructor({ + field, + value, + index, + disabled = false, + negate = false, + }: { + field: string; + value: string | number; + index: string; + disabled?: boolean; + negate?: boolean; + }) { + this.field = field; + this.value = value; + this.disabled = disabled; + this.index = index; + this.negate = negate; + } + + public getDisplayText = () => { + return `${this.field} : ${this.value}`; + }; +} diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 563fb8884b6f61..8e84bc993c5890 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -18,10 +18,13 @@ */ import React, { Component } from 'react'; +import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; export class FilterBar extends Component { public render() { - return ; + return ( + + ); } } diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index 5e0369e37255af..09b349d1462451 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -17,10 +17,13 @@ * under the License. */ -import React, { Component } from 'react'; +import React, { SFC } from 'react'; +import { Filter } from 'ui/filter_bar/filters'; -export class FilterItem extends Component { - public render() { - return
Hello world
; - } +interface Props { + filter: Filter; } + +export const FilterItem: SFC = props => { + return
{props.filter.getDisplayText()}
; +}; From fceda5f9d7b045edf4f271b4582df771765cbe3e Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Wed, 7 Nov 2018 17:25:25 -0500 Subject: [PATCH 05/96] refine usage of Filter class in FilterBar --- .../filter_bar_phrase_filter.tsx | 29 +++++ .../filters/filter_bar_filters/index.ts | 35 +++++ src/ui/public/filter_bar/filters/index.ts | 24 +++- .../filter_bar/filters/phrase_filter.ts | 45 +++---- src/ui/public/filter_bar/react/filter_bar.tsx | 19 ++- .../public/filter_bar/react/filter_item.tsx | 122 +++++++++++++++++- 6 files changed, 236 insertions(+), 38 deletions(-) create mode 100644 src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx create mode 100644 src/ui/public/filter_bar/filters/filter_bar_filters/index.ts diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx b/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx new file mode 100644 index 00000000000000..a3b2bfa23b81e7 --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; + +export function createFilterBarPhraseFilter(filter: PhraseFilter) { + return { + ...filter, + getDisplayText: () => { + return `${filter.field} : ${filter.value}`; + }, + }; +} diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts new file mode 100644 index 00000000000000..6f44a6fa9df409 --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filter } from 'ui/filter_bar/filters'; +import { createFilterBarPhraseFilter } from 'ui/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter'; +import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; + +export type FilterBarFilter = Filter & { + getDisplayText: () => string; +}; + +export function createFilterBarFilter(filter: Filter): FilterBarFilter { + switch (filter.type) { + case 'PhraseFilter': + return createFilterBarPhraseFilter(filter as PhraseFilter); + default: + throw new Error(`Unknown filter type: ${filter.type}`); + } +} diff --git a/src/ui/public/filter_bar/filters/index.ts b/src/ui/public/filter_bar/filters/index.ts index e317ef5827db23..a15df0513c747b 100644 --- a/src/ui/public/filter_bar/filters/index.ts +++ b/src/ui/public/filter_bar/filters/index.ts @@ -18,8 +18,30 @@ */ export interface Filter { + type: string; disabled: boolean; negate: boolean; + pinned: boolean; index: string; - getDisplayText: () => string; +} + +export interface CreateFilterOptions { + disabled?: boolean; + negate?: boolean; + pinned?: boolean; + index: string; +} + +export function createFilter({ + index, + negate = false, + disabled = false, + pinned = false, +}: CreateFilterOptions) { + return { + index, + negate, + disabled, + pinned, + }; } diff --git a/src/ui/public/filter_bar/filters/phrase_filter.ts b/src/ui/public/filter_bar/filters/phrase_filter.ts index e123bb21ecc5ff..31f4d6ed81bba3 100644 --- a/src/ui/public/filter_bar/filters/phrase_filter.ts +++ b/src/ui/public/filter_bar/filters/phrase_filter.ts @@ -17,36 +17,27 @@ * under the License. */ -import { Filter } from 'ui/filter_bar/filters/index'; +import { createFilter, CreateFilterOptions, Filter } from 'ui/filter_bar/filters/index'; -export class PhraseFilter implements Filter { - public field: string; - public value: string | number; - public disabled: boolean; - public index: string; - public negate: boolean; +export type PhraseFilter = Filter & { + field: string; + value: string | number; +}; - constructor({ +interface CreatePhraseFilterOptions { + field: string; + value: string | number; +} + +export function createPhraseFilter( + options: CreatePhraseFilterOptions & CreateFilterOptions +): PhraseFilter { + const baseFilter = createFilter(options); + const { field, value } = options; + return { + ...baseFilter, + type: 'PhraseFilter', field, value, - index, - disabled = false, - negate = false, - }: { - field: string; - value: string | number; - index: string; - disabled?: boolean; - negate?: boolean; - }) { - this.field = field; - this.value = value; - this.disabled = disabled; - this.index = index; - this.negate = negate; - } - - public getDisplayText = () => { - return `${this.field} : ${this.value}`; }; } diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 8e84bc993c5890..77ccde1fc65a79 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -18,13 +18,24 @@ */ import React, { Component } from 'react'; -import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; +import { createFilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; +import { createPhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; +const filters = [ + createPhraseFilter({ field: 'response', value: 200, index: 'foo' }), + createPhraseFilter({ field: 'extension', value: 'jpg', index: 'foo' }), + createPhraseFilter({ field: 'bytes', value: 2000, index: 'foo' }), +]; + +const filterBarFilters = filters.map(createFilterBarFilter); + export class FilterBar extends Component { public render() { - return ( - - ); + const filterItems = filterBarFilters.map(filterBarFilter => { + return ; + }); + + return filterItems; } } diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index 09b349d1462451..a52814fb9fd96c 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -17,13 +17,123 @@ * under the License. */ -import React, { SFC } from 'react'; -import { Filter } from 'ui/filter_bar/filters'; +import { EuiBadge, EuiContextMenu, EuiPopover } from '@elastic/eui'; +import classNames from 'classnames'; +import React, { Component } from 'react'; +import { FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; interface Props { - filter: Filter; + filter: FilterBarFilter; + className?: string; } -export const FilterItem: SFC = props => { - return
{props.filter.getDisplayText()}
; -}; +interface State { + isPopoverOpen: boolean; +} + +export class FilterItem extends Component { + public state = { + isPopoverOpen: false, + }; + + public render() { + const classes = classNames( + 'globalFilterItem', + { + 'globalFilterItem-isDisabled': this.props.filter.disabled, + 'globalFilterItem-isPinned': false, + 'globalFilterItem-isExcluded': this.props.filter.negate, + }, + this.props.className + ); + + let prefix = null; + if (this.props.filter.negate) { + prefix = NOT ; + } + + const badge = ( + { + return; + }} + iconOnClickAriaLabel={`Delete filter`} + iconType="cross" + // @ts-ignore + iconSide="right" + onClick={this.togglePopover} + onClickAriaLabel="Filter actions" + closeButtonProps={{ + // Removing tab focus on close button because the same option can be optained through the context menu + // Also, we may want to add a `DEL` keyboard press functionality + tabIndex: '-1', + }} + > + {prefix} + {this.props.filter.getDisplayText()} + + ); + + const panelTree = { + id: 0, + items: [ + { + name: `${this.props.filter.pinned ? 'Unpin' : 'Pin across all apps'}`, + icon: 'pin', + onClick: () => { + this.closePopover(); + }, + }, + { + name: `${this.props.filter.negate ? 'Include results' : 'Exclude results'}`, + icon: `${this.props.filter.negate ? 'plusInCircle' : 'minusInCircle'}`, + onClick: () => { + this.closePopover(); + }, + }, + { + name: `${this.props.filter.disabled ? 'Re-enable' : 'Temporarily disable'}`, + icon: `${this.props.filter.disabled ? 'eye' : 'eyeClosed'}`, + onClick: () => { + this.closePopover(); + }, + }, + { + name: 'Delete', + icon: 'trash', + onClick: () => { + this.closePopover(); + }, + }, + ], + }; + + return ( + + + + ); + } + + private closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + private togglePopover = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + }; +} From 648081e96a37ed014408dfca808dea5f32ccfcbe Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Thu, 8 Nov 2018 15:19:59 -0500 Subject: [PATCH 06/96] wire up the callbacks for the FilterItem --- .../public/discover/controllers/discover.js | 32 +++++++++++++++ .../kibana/public/discover/index.html | 20 ++++++++- .../filter_bar_phrase_filter.tsx | 2 +- .../filters/filter_bar_filters/index.ts | 5 ++- src/ui/public/filter_bar/react/filter_bar.tsx | 41 ++++++++++++++----- .../public/filter_bar/react/filter_item.tsx | 31 +++++++++----- 6 files changed, 105 insertions(+), 26 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 1965870d5ef1c3..2f469c727c5261 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -65,6 +65,7 @@ import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; +import { createPhraseFilter } from '../../../../../ui/public/filter_bar/filters/phrase_filter'; const app = uiModules.get('apps/discover', [ 'kibana/notify', @@ -182,6 +183,37 @@ function discoverController( requests: new RequestAdapter() }; + + $scope.reactFilters = [ + createPhraseFilter({ field: 'response', value: 200, index: 'foo' }), + createPhraseFilter({ field: 'extension', value: 'jpg', index: 'foo' }), + createPhraseFilter({ field: 'bytes', value: 2000, index: 'foo' }), + ]; + + $scope.onToggleNegate = (filter) => { + const index = $scope.reactFilters.indexOf(filter); + $scope.reactFilters[index] = { + ...filter, + negate: !filter.negate + }; + }; + + $scope.onTogglePin = (filter) => { + const index = $scope.reactFilters.indexOf(filter); + $scope.reactFilters[index] = { + ...filter, + pinned: !filter.pinned + }; + }; + + $scope.onToggleDisabled = (filter) => { + const index = $scope.reactFilters.indexOf(filter); + $scope.reactFilters[index] = { + ...filter, + disabled: !filter.disabled + }; + }; + $scope.getDocLink = getDocLink; $scope.intervalOptions = intervalOptions; $scope.showInterval = false; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index 393836bc259b86..c0e57f30b42396 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -40,7 +40,12 @@

- + ng-hide="fetchError" class="dscOverlay" > +<<<<<<< HEAD:src/legacy/core_plugins/kibana/public/discover/index.html

+======= +
+

Searching

+>>>>>>> wire up the callbacks for the FilterItem:src/core_plugins/kibana/public/discover/index.html
{{fetchStatus.complete}}/{{fetchStatus.total}}
@@ -139,7 +149,7 @@

ng-options="interval.val as interval.display for interval in intervalOptions | filter: intervalEnabled" ng-blur="toggleInterval()" data-test-subj="discoverIntervalSelect" - > + >
+======= + ng-if="vis && rows.length !== 0" + style="display: flex; height: 200px" + > +>>>>>>> wire up the callbacks for the FilterItem:src/core_plugins/kibana/public/discover/index.html
diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx b/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx index a3b2bfa23b81e7..d5e85835f75005 100644 --- a/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx +++ b/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx @@ -21,7 +21,7 @@ import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; export function createFilterBarPhraseFilter(filter: PhraseFilter) { return { - ...filter, + filter, getDisplayText: () => { return `${filter.field} : ${filter.value}`; }, diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts index 6f44a6fa9df409..5c7fdc4e8fb4cc 100644 --- a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts +++ b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts @@ -21,9 +21,10 @@ import { Filter } from 'ui/filter_bar/filters'; import { createFilterBarPhraseFilter } from 'ui/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter'; import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; -export type FilterBarFilter = Filter & { +export interface FilterBarFilter { + filter: Filter; getDisplayText: () => string; -}; +} export function createFilterBarFilter(filter: Filter): FilterBarFilter { switch (filter.type) { diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 77ccde1fc65a79..7077565bc80764 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -18,22 +18,41 @@ */ import React, { Component } from 'react'; -import { createFilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; -import { createPhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; +import { Filter } from 'ui/filter_bar/filters'; +import { createFilterBarFilter, FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; -const filters = [ - createPhraseFilter({ field: 'response', value: 200, index: 'foo' }), - createPhraseFilter({ field: 'extension', value: 'jpg', index: 'foo' }), - createPhraseFilter({ field: 'bytes', value: 2000, index: 'foo' }), -]; +interface Props { + filters: Filter[]; + onToggleNegate: (filter: Filter) => void; + onToggleDisabled: (filter: Filter) => void; + onTogglePin: (filter: Filter) => void; +} + +export class FilterBar extends Component { + public onToggleNegate = (filter: FilterBarFilter) => { + this.props.onToggleNegate(filter.filter); + }; + + public onTogglePin = (filter: FilterBarFilter) => { + this.props.onTogglePin(filter.filter); + }; -const filterBarFilters = filters.map(createFilterBarFilter); + public onToggleDisabled = (filter: FilterBarFilter) => { + this.props.onToggleDisabled(filter.filter); + }; -export class FilterBar extends Component { public render() { - const filterItems = filterBarFilters.map(filterBarFilter => { - return ; + const filterItems = this.props.filters.map(filter => { + const filterBarFilter = createFilterBarFilter(filter); + return ( + + ); }); return filterItems; diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index a52814fb9fd96c..42c472d34eba15 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -25,6 +25,9 @@ import { FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; interface Props { filter: FilterBarFilter; className?: string; + onTogglePin: (filter: FilterBarFilter) => void; + onToggleNegate: (filter: FilterBarFilter) => void; + onToggleDisabled: (filter: FilterBarFilter) => void; } interface State { @@ -37,18 +40,23 @@ export class FilterItem extends Component { }; public render() { + const filter = this.props.filter; + const { + filter: { negate, disabled, pinned }, + } = filter; + const classes = classNames( 'globalFilterItem', { - 'globalFilterItem-isDisabled': this.props.filter.disabled, + 'globalFilterItem-isDisabled': disabled, 'globalFilterItem-isPinned': false, - 'globalFilterItem-isExcluded': this.props.filter.negate, + 'globalFilterItem-isExcluded': negate, }, this.props.className ); let prefix = null; - if (this.props.filter.negate) { + if (negate) { prefix = NOT ; } @@ -73,7 +81,7 @@ export class FilterItem extends Component { }} > {prefix} - {this.props.filter.getDisplayText()} + {filter.getDisplayText()} ); @@ -81,24 +89,27 @@ export class FilterItem extends Component { id: 0, items: [ { - name: `${this.props.filter.pinned ? 'Unpin' : 'Pin across all apps'}`, + name: `${pinned ? 'Unpin' : 'Pin across all apps'}`, icon: 'pin', onClick: () => { this.closePopover(); + this.props.onTogglePin(filter); }, }, { - name: `${this.props.filter.negate ? 'Include results' : 'Exclude results'}`, - icon: `${this.props.filter.negate ? 'plusInCircle' : 'minusInCircle'}`, + name: `${negate ? 'Include results' : 'Exclude results'}`, + icon: `${negate ? 'plusInCircle' : 'minusInCircle'}`, onClick: () => { this.closePopover(); + this.props.onToggleNegate(filter); }, }, { - name: `${this.props.filter.disabled ? 'Re-enable' : 'Temporarily disable'}`, - icon: `${this.props.filter.disabled ? 'eye' : 'eyeClosed'}`, + name: `${disabled ? 'Re-enable' : 'Temporarily disable'}`, + icon: `${disabled ? 'eye' : 'eyeClosed'}`, onClick: () => { this.closePopover(); + this.props.onToggleDisabled(filter); }, }, { @@ -113,7 +124,7 @@ export class FilterItem extends Component { return ( Date: Thu, 8 Nov 2018 15:25:54 -0500 Subject: [PATCH 07/96] forgot filter deletion --- .../kibana/public/discover/controllers/discover.js | 4 ++++ src/legacy/core_plugins/kibana/public/discover/index.html | 1 + src/ui/public/filter_bar/react/filter_bar.tsx | 6 ++++++ src/ui/public/filter_bar/react/filter_item.tsx | 2 ++ 4 files changed, 13 insertions(+) diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 2f469c727c5261..6e9544cde3e52d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -214,6 +214,10 @@ function discoverController( }; }; + $scope.onDelete = (filterToDelete) => { + $scope.reactFilters = $scope.reactFilters.filter((filter) => filter !== filterToDelete); + }; + $scope.getDocLink = getDocLink; $scope.intervalOptions = intervalOptions; $scope.showInterval = false; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index c0e57f30b42396..141b788d8e3a7a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -45,6 +45,7 @@

on-toggle-negate="onToggleNegate" on-toggle-pin="onTogglePin" on-toggle-disabled="onToggleDisabled" + on-delete="onDelete" > void; onToggleDisabled: (filter: Filter) => void; onTogglePin: (filter: Filter) => void; + onDelete: (filter: Filter) => void; } export class FilterBar extends Component { @@ -42,6 +43,10 @@ export class FilterBar extends Component { this.props.onToggleDisabled(filter.filter); }; + public onDelete = (filter: FilterBarFilter) => { + this.props.onDelete(filter.filter); + }; + public render() { const filterItems = this.props.filters.map(filter => { const filterBarFilter = createFilterBarFilter(filter); @@ -51,6 +56,7 @@ export class FilterBar extends Component { onToggleNegate={this.onToggleNegate} onToggleDisabled={this.onToggleDisabled} onTogglePin={this.onTogglePin} + onDelete={this.onDelete} /> ); }); diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index 42c472d34eba15..d7fb8274c67ab5 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -28,6 +28,7 @@ interface Props { onTogglePin: (filter: FilterBarFilter) => void; onToggleNegate: (filter: FilterBarFilter) => void; onToggleDisabled: (filter: FilterBarFilter) => void; + onDelete: (filter: FilterBarFilter) => void; } interface State { @@ -117,6 +118,7 @@ export class FilterItem extends Component { icon: 'trash', onClick: () => { this.closePopover(); + this.props.onDelete(filter); }, }, ], From 0dce26d01755e7b8535a10d0f55e39b6b057df08 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Mon, 12 Nov 2018 16:23:51 -0500 Subject: [PATCH 08/96] clean up filter refactorings --- .../filters/filter_bar_filters/index.ts | 20 +++++++---- src/ui/public/filter_bar/filters/index.ts | 26 +-------------- ...r_bar_phrase_filter.tsx => meta_filter.ts} | 33 ++++++++++++++++--- .../filter_bar/filters/phrase_filter.ts | 12 +++---- src/ui/public/filter_bar/react/filter_bar.tsx | 3 +- 5 files changed, 51 insertions(+), 43 deletions(-) rename src/ui/public/filter_bar/filters/{filter_bar_filters/filter_bar_phrase_filter.tsx => meta_filter.ts} (53%) diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts index 5c7fdc4e8fb4cc..5a82d28201287f 100644 --- a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts +++ b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts @@ -17,19 +17,25 @@ * under the License. */ -import { Filter } from 'ui/filter_bar/filters'; -import { createFilterBarPhraseFilter } from 'ui/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter'; +import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; -export interface FilterBarFilter { - filter: Filter; +export type FilterBarFilter = MetaFilter & { getDisplayText: () => string; -} +}; + +export function createFilterBarFilter(metaFilter: MetaFilter): FilterBarFilter { + const filter = metaFilter.filter; -export function createFilterBarFilter(filter: Filter): FilterBarFilter { switch (filter.type) { case 'PhraseFilter': - return createFilterBarPhraseFilter(filter as PhraseFilter); + return { + ...metaFilter, + getDisplayText() { + const { field, value } = this.filter as PhraseFilter; + return `${field} : ${value}`; + }, + }; default: throw new Error(`Unknown filter type: ${filter.type}`); } diff --git a/src/ui/public/filter_bar/filters/index.ts b/src/ui/public/filter_bar/filters/index.ts index a15df0513c747b..7dc64c1fa6a228 100644 --- a/src/ui/public/filter_bar/filters/index.ts +++ b/src/ui/public/filter_bar/filters/index.ts @@ -19,29 +19,5 @@ export interface Filter { type: string; - disabled: boolean; - negate: boolean; - pinned: boolean; - index: string; -} - -export interface CreateFilterOptions { - disabled?: boolean; - negate?: boolean; - pinned?: boolean; - index: string; -} - -export function createFilter({ - index, - negate = false, - disabled = false, - pinned = false, -}: CreateFilterOptions) { - return { - index, - negate, - disabled, - pinned, - }; + toElasticsearchQuery: () => any; } diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx b/src/ui/public/filter_bar/filters/meta_filter.ts similarity index 53% rename from src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx rename to src/ui/public/filter_bar/filters/meta_filter.ts index d5e85835f75005..90fdca90d063a8 100644 --- a/src/ui/public/filter_bar/filters/filter_bar_filters/filter_bar_phrase_filter.tsx +++ b/src/ui/public/filter_bar/filters/meta_filter.ts @@ -17,13 +17,38 @@ * under the License. */ -import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; +import { Filter } from 'ui/filter_bar/filters/index'; -export function createFilterBarPhraseFilter(filter: PhraseFilter) { +export interface MetaFilter { + filter: Filter; + disabled: boolean; + negate: boolean; + pinned: boolean; + index?: string; + toElasticsearchQuery: () => void; +} + +interface CreateMetaFilterOptions { + disabled?: boolean; + negate?: boolean; + pinned?: boolean; + index?: string; +} + +export function createMetaFilter( + filter: Filter, + { disabled = false, negate = false, pinned = false, index }: CreateMetaFilterOptions = {} +): MetaFilter { return { filter, - getDisplayText: () => { - return `${filter.field} : ${filter.value}`; + disabled, + negate, + pinned, + index, + toElasticsearchQuery: () => { + // TODO implement me + // if negate === true then wrap filter in a `not` filter + // call underlying filter's toElasticsearchQuery }, }; } diff --git a/src/ui/public/filter_bar/filters/phrase_filter.ts b/src/ui/public/filter_bar/filters/phrase_filter.ts index 31f4d6ed81bba3..ddb3c4845a54b0 100644 --- a/src/ui/public/filter_bar/filters/phrase_filter.ts +++ b/src/ui/public/filter_bar/filters/phrase_filter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createFilter, CreateFilterOptions, Filter } from 'ui/filter_bar/filters/index'; +import { Filter } from 'ui/filter_bar/filters/index'; export type PhraseFilter = Filter & { field: string; @@ -29,15 +29,15 @@ interface CreatePhraseFilterOptions { value: string | number; } -export function createPhraseFilter( - options: CreatePhraseFilterOptions & CreateFilterOptions -): PhraseFilter { - const baseFilter = createFilter(options); +export function createPhraseFilter(options: CreatePhraseFilterOptions): PhraseFilter { const { field, value } = options; return { - ...baseFilter, type: 'PhraseFilter', field, value, + toElasticsearchQuery() { + // TODO implement me + return {}; + }, }; } diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 5744416ce62314..bd8ebfcf41e2c9 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -20,6 +20,7 @@ import React, { Component } from 'react'; import { Filter } from 'ui/filter_bar/filters'; import { createFilterBarFilter, FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; +import { createMetaFilter } from 'ui/filter_bar/filters/meta_filter'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; interface Props { @@ -49,7 +50,7 @@ export class FilterBar extends Component { public render() { const filterItems = this.props.filters.map(filter => { - const filterBarFilter = createFilterBarFilter(filter); + const filterBarFilter = createFilterBarFilter(createMetaFilter(filter)); return ( Date: Mon, 12 Nov 2018 16:35:40 -0500 Subject: [PATCH 09/96] hook up delete button --- src/ui/public/filter_bar/react/filter_item.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index d7fb8274c67ab5..049181db438f49 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -66,9 +66,7 @@ export class FilterItem extends Component { id={'foo'} className={classes} title={'foo'} - iconOnClick={() => { - return; - }} + iconOnClick={() => this.props.onDelete(filter)} iconOnClickAriaLabel={`Delete filter`} iconType="cross" // @ts-ignore From 79ed1628bc55925d6689587398aa62de30a78ed6 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 13 Nov 2018 10:26:57 -0500 Subject: [PATCH 10/96] fix callbacks, try Object.create --- .../public/discover/controllers/discover.js | 26 ++++------ .../filters/filter_bar_filters/index.ts | 15 ++---- .../public/filter_bar/filters/meta_filter.ts | 52 ++++++++++++++----- src/ui/public/filter_bar/react/filter_bar.tsx | 23 ++++---- .../public/filter_bar/react/filter_item.tsx | 6 +-- 5 files changed, 67 insertions(+), 55 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 6e9544cde3e52d..c32922ae6981f3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -66,6 +66,11 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; import { createPhraseFilter } from '../../../../../ui/public/filter_bar/filters/phrase_filter'; +import { + createMetaFilter, + toggleDisabled, + toggleNegation, togglePinned, +} from '../../../../../ui/public/filter_bar/filters/meta_filter'; const app = uiModules.get('apps/discover', [ 'kibana/notify', @@ -185,33 +190,24 @@ function discoverController( $scope.reactFilters = [ - createPhraseFilter({ field: 'response', value: 200, index: 'foo' }), - createPhraseFilter({ field: 'extension', value: 'jpg', index: 'foo' }), - createPhraseFilter({ field: 'bytes', value: 2000, index: 'foo' }), + createMetaFilter(createPhraseFilter({ field: 'response', value: 200, index: 'foo' })), + createMetaFilter(createPhraseFilter({ field: 'extension', value: 'jpg', index: 'foo' })), + createMetaFilter(createPhraseFilter({ field: 'bytes', value: 2000, index: 'foo' })), ]; $scope.onToggleNegate = (filter) => { const index = $scope.reactFilters.indexOf(filter); - $scope.reactFilters[index] = { - ...filter, - negate: !filter.negate - }; + $scope.reactFilters[index] = toggleNegation(filter); }; $scope.onTogglePin = (filter) => { const index = $scope.reactFilters.indexOf(filter); - $scope.reactFilters[index] = { - ...filter, - pinned: !filter.pinned - }; + $scope.reactFilters[index] = togglePinned(filter); }; $scope.onToggleDisabled = (filter) => { const index = $scope.reactFilters.indexOf(filter); - $scope.reactFilters[index] = { - ...filter, - disabled: !filter.disabled - }; + $scope.reactFilters[index] = toggleDisabled(filter); }; $scope.onDelete = (filterToDelete) => { diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts index 5a82d28201287f..779b6e6ced768f 100644 --- a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts +++ b/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts @@ -18,24 +18,19 @@ */ import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; -import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; export type FilterBarFilter = MetaFilter & { getDisplayText: () => string; }; -export function createFilterBarFilter(metaFilter: MetaFilter): FilterBarFilter { - const filter = metaFilter.filter; - +export function createFilterBarFilter(filter: MetaFilter): FilterBarFilter { switch (filter.type) { case 'PhraseFilter': - return { - ...metaFilter, - getDisplayText() { - const { field, value } = this.filter as PhraseFilter; - return `${field} : ${value}`; - }, + const filterBarFilter = Object.create(filter); + filterBarFilter.getDisplayText = function() { + return `${this.field} : ${this.value}`; }; + return filterBarFilter; default: throw new Error(`Unknown filter type: ${filter.type}`); } diff --git a/src/ui/public/filter_bar/filters/meta_filter.ts b/src/ui/public/filter_bar/filters/meta_filter.ts index 90fdca90d063a8..79dab38cf561ae 100644 --- a/src/ui/public/filter_bar/filters/meta_filter.ts +++ b/src/ui/public/filter_bar/filters/meta_filter.ts @@ -19,14 +19,13 @@ import { Filter } from 'ui/filter_bar/filters/index'; -export interface MetaFilter { - filter: Filter; +export type MetaFilter = Filter & { disabled: boolean; negate: boolean; pinned: boolean; index?: string; toElasticsearchQuery: () => void; -} +}; interface CreateMetaFilterOptions { disabled?: boolean; @@ -39,16 +38,41 @@ export function createMetaFilter( filter: Filter, { disabled = false, negate = false, pinned = false, index }: CreateMetaFilterOptions = {} ): MetaFilter { - return { - filter, - disabled, - negate, - pinned, - index, - toElasticsearchQuery: () => { - // TODO implement me - // if negate === true then wrap filter in a `not` filter - // call underlying filter's toElasticsearchQuery - }, + const metaFilter = Object.create(filter); + metaFilter.disabled = disabled; + metaFilter.negate = negate; + metaFilter.pinned = pinned; + metaFilter.index = index; + metaFilter.toElasticsearchQuery = function() { + return this; + // TODO implement me + // if negate === true then wrap filter in a `not` filter + // call underlying filter's toElasticsearchQuery + // e.g. Object.getPrototypeOf(this).toElasticsearchQuery(); + }; + return metaFilter; +} + +export function toggleNegation(filter: MetaFilter) { + const meta = { + ...filter, + negate: !filter.negate, + }; + return createMetaFilter(Object.getPrototypeOf(filter), meta); +} + +export function togglePinned(filter: MetaFilter) { + const meta = { + ...filter, + pinned: !filter.pinned, + }; + return createMetaFilter(Object.getPrototypeOf(filter), meta); +} + +export function toggleDisabled(filter: MetaFilter) { + const meta = { + ...filter, + disabled: !filter.disabled, }; + return createMetaFilter(Object.getPrototypeOf(filter), meta); } diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index bd8ebfcf41e2c9..2a8999be99dab9 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -18,39 +18,38 @@ */ import React, { Component } from 'react'; -import { Filter } from 'ui/filter_bar/filters'; import { createFilterBarFilter, FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; -import { createMetaFilter } from 'ui/filter_bar/filters/meta_filter'; +import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; interface Props { - filters: Filter[]; - onToggleNegate: (filter: Filter) => void; - onToggleDisabled: (filter: Filter) => void; - onTogglePin: (filter: Filter) => void; - onDelete: (filter: Filter) => void; + filters: MetaFilter[]; + onToggleNegate: (filter: MetaFilter) => void; + onToggleDisabled: (filter: MetaFilter) => void; + onTogglePin: (filter: MetaFilter) => void; + onDelete: (filter: MetaFilter) => void; } export class FilterBar extends Component { public onToggleNegate = (filter: FilterBarFilter) => { - this.props.onToggleNegate(filter.filter); + this.props.onToggleNegate(Object.getPrototypeOf(filter)); }; public onTogglePin = (filter: FilterBarFilter) => { - this.props.onTogglePin(filter.filter); + this.props.onTogglePin(Object.getPrototypeOf(filter)); }; public onToggleDisabled = (filter: FilterBarFilter) => { - this.props.onToggleDisabled(filter.filter); + this.props.onToggleDisabled(Object.getPrototypeOf(filter)); }; public onDelete = (filter: FilterBarFilter) => { - this.props.onDelete(filter.filter); + this.props.onDelete(Object.getPrototypeOf(filter)); }; public render() { const filterItems = this.props.filters.map(filter => { - const filterBarFilter = createFilterBarFilter(createMetaFilter(filter)); + const filterBarFilter = createFilterBarFilter(filter); return ( { public render() { const filter = this.props.filter; - const { - filter: { negate, disabled, pinned }, - } = filter; + const { negate, disabled, pinned } = filter; const classes = classNames( 'globalFilterItem', { 'globalFilterItem-isDisabled': disabled, - 'globalFilterItem-isPinned': false, + 'globalFilterItem-isPinned': pinned, 'globalFilterItem-isExcluded': negate, }, this.props.className From f76a0eecdc5959467ad4d7f048e0ae5d848637f0 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 13 Nov 2018 11:31:59 -0500 Subject: [PATCH 11/96] move filter_bar_filter.js up a directory --- .../{filter_bar_filters/index.ts => filter_bar_filter.ts} | 0 src/ui/public/filter_bar/react/filter_bar.tsx | 2 +- src/ui/public/filter_bar/react/filter_item.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/ui/public/filter_bar/filters/{filter_bar_filters/index.ts => filter_bar_filter.ts} (100%) diff --git a/src/ui/public/filter_bar/filters/filter_bar_filters/index.ts b/src/ui/public/filter_bar/filters/filter_bar_filter.ts similarity index 100% rename from src/ui/public/filter_bar/filters/filter_bar_filters/index.ts rename to src/ui/public/filter_bar/filters/filter_bar_filter.ts diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 2a8999be99dab9..fddae6cbda7b9f 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -18,7 +18,7 @@ */ import React, { Component } from 'react'; -import { createFilterBarFilter, FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; +import { createFilterBarFilter, FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filter'; import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index 47376a2d0c314f..a8b415c1e05c38 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -20,7 +20,7 @@ import { EuiBadge, EuiContextMenu, EuiPopover } from '@elastic/eui'; import classNames from 'classnames'; import React, { Component } from 'react'; -import { FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filters'; +import { FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filter'; interface Props { filter: FilterBarFilter; From f302424f61787637b386515c791b58f11c3ba5df Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 13 Nov 2018 12:36:07 -0500 Subject: [PATCH 12/96] enable easy changes to the underlying metafilter filter --- .../public/filter_bar/filters/meta_filter.ts | 40 ++++++++++++------- .../filter_bar/filters/phrase_filter.ts | 17 ++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/ui/public/filter_bar/filters/meta_filter.ts b/src/ui/public/filter_bar/filters/meta_filter.ts index 79dab38cf561ae..e2a697495ea4b2 100644 --- a/src/ui/public/filter_bar/filters/meta_filter.ts +++ b/src/ui/public/filter_bar/filters/meta_filter.ts @@ -17,6 +17,7 @@ * under the License. */ +import { isEmpty, omit, pick } from 'lodash'; import { Filter } from 'ui/filter_bar/filters/index'; export type MetaFilter = Filter & { @@ -25,6 +26,7 @@ export type MetaFilter = Filter & { pinned: boolean; index?: string; toElasticsearchQuery: () => void; + applyChanges: (updateObject: Partial) => MetaFilter; }; interface CreateMetaFilterOptions { @@ -50,29 +52,37 @@ export function createMetaFilter( // call underlying filter's toElasticsearchQuery // e.g. Object.getPrototypeOf(this).toElasticsearchQuery(); }; + metaFilter.applyChanges = function(updateObject: Partial) { + if (isEmpty(updateObject)) { + return this; + } + + const metaProps = ['disabled', 'negate', 'pinned', 'index']; + const currentProps = pick(this, metaProps); + const metaChanges = pick(updateObject, metaProps); + const otherChanges = omit(updateObject, metaProps); + const updatedFilter = Object.getPrototypeOf(this).applyChanges(otherChanges); + const mergedProps = { + ...currentProps, + ...metaChanges, + }; + + return createMetaFilter(updatedFilter, mergedProps); + }; return metaFilter; } export function toggleNegation(filter: MetaFilter) { - const meta = { - ...filter, - negate: !filter.negate, - }; - return createMetaFilter(Object.getPrototypeOf(filter), meta); + const negate = !filter.negate; + return filter.applyChanges({ negate }); } export function togglePinned(filter: MetaFilter) { - const meta = { - ...filter, - pinned: !filter.pinned, - }; - return createMetaFilter(Object.getPrototypeOf(filter), meta); + const pinned = !filter.pinned; + return filter.applyChanges({ pinned }); } export function toggleDisabled(filter: MetaFilter) { - const meta = { - ...filter, - disabled: !filter.disabled, - }; - return createMetaFilter(Object.getPrototypeOf(filter), meta); + const disabled = !filter.disabled; + return filter.applyChanges({ disabled }); } diff --git a/src/ui/public/filter_bar/filters/phrase_filter.ts b/src/ui/public/filter_bar/filters/phrase_filter.ts index ddb3c4845a54b0..23b80e0614625c 100644 --- a/src/ui/public/filter_bar/filters/phrase_filter.ts +++ b/src/ui/public/filter_bar/filters/phrase_filter.ts @@ -17,11 +17,13 @@ * under the License. */ +import { isEmpty, pick } from 'lodash'; import { Filter } from 'ui/filter_bar/filters/index'; export type PhraseFilter = Filter & { field: string; value: string | number; + applyChanges: (updateObject: Partial) => PhraseFilter; }; interface CreatePhraseFilterOptions { @@ -39,5 +41,20 @@ export function createPhraseFilter(options: CreatePhraseFilterOptions): PhraseFi // TODO implement me return {}; }, + applyChanges(updateObject: Partial) { + if (isEmpty(updateObject)) { + return this; + } + + const props = ['field', 'value']; + const updatedProps = pick(updateObject, props); + const currentProps = pick(this, props); + const mergedProps = { + ...currentProps, + ...updatedProps, + } as CreatePhraseFilterOptions; + + return createPhraseFilter(mergedProps); + }, }; } From fe3c0ad14e159c0b7b9f5a943b781fc13a23b3ef Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 13 Nov 2018 16:09:36 -0500 Subject: [PATCH 13/96] establishing pattern for creating views of filters --- .../filter_bar/filters/filter_views/index.ts | 36 ++++++++ .../phrase_filter_views.tsx} | 28 +++---- .../public/filter_bar/filters/meta_filter.ts | 83 ++++++++++--------- src/ui/public/filter_bar/react/filter_bar.tsx | 20 ++--- .../public/filter_bar/react/filter_item.tsx | 23 ++--- 5 files changed, 110 insertions(+), 80 deletions(-) create mode 100644 src/ui/public/filter_bar/filters/filter_views/index.ts rename src/ui/public/filter_bar/filters/{filter_bar_filter.ts => filter_views/phrase_filter_views.tsx} (60%) diff --git a/src/ui/public/filter_bar/filters/filter_views/index.ts b/src/ui/public/filter_bar/filters/filter_views/index.ts new file mode 100644 index 00000000000000..03349fc1cf5624 --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/index.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MetaFilter } from 'src/ui/public/filter_bar/filters/meta_filter'; +import { Filter } from 'ui/filter_bar/filters'; +import { PhraseFilterViews } from './phrase_filter_views'; + +const filterViews: { [index: string]: FilterViews } = { + PhraseFilter: PhraseFilterViews, +}; + +export interface FilterViews { + getDisplayText: (filter: Filter) => string; +} + +export function getFilterDisplayText(metaFilter: MetaFilter): string { + const prefix = metaFilter.negate ? 'NOT ' : ''; + const filterText = filterViews[metaFilter.filter.type].getDisplayText(metaFilter.filter); + return `${prefix}${filterText}`; +} diff --git a/src/ui/public/filter_bar/filters/filter_bar_filter.ts b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx similarity index 60% rename from src/ui/public/filter_bar/filters/filter_bar_filter.ts rename to src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx index 779b6e6ced768f..208598d4a7d2fe 100644 --- a/src/ui/public/filter_bar/filters/filter_bar_filter.ts +++ b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx @@ -17,21 +17,19 @@ * under the License. */ -import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; +import { Filter } from 'ui/filter_bar/filters'; +import { FilterViews } from 'ui/filter_bar/filters/filter_views/index'; +import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; -export type FilterBarFilter = MetaFilter & { - getDisplayText: () => string; -}; - -export function createFilterBarFilter(filter: MetaFilter): FilterBarFilter { - switch (filter.type) { - case 'PhraseFilter': - const filterBarFilter = Object.create(filter); - filterBarFilter.getDisplayText = function() { - return `${this.field} : ${this.value}`; - }; - return filterBarFilter; - default: - throw new Error(`Unknown filter type: ${filter.type}`); +function getDisplayText(filter: Filter) { + if (filter.type === 'PhraseFilter') { + const { field, value } = filter as PhraseFilter; + return `${field} : ${value}`; + } else { + throw new Error(`${filter.type} is not a PhraseFilter`); } } + +export const PhraseFilterViews: FilterViews = { + getDisplayText, +}; diff --git a/src/ui/public/filter_bar/filters/meta_filter.ts b/src/ui/public/filter_bar/filters/meta_filter.ts index e2a697495ea4b2..7b6e2e2f1d7da5 100644 --- a/src/ui/public/filter_bar/filters/meta_filter.ts +++ b/src/ui/public/filter_bar/filters/meta_filter.ts @@ -17,17 +17,16 @@ * under the License. */ -import { isEmpty, omit, pick } from 'lodash'; import { Filter } from 'ui/filter_bar/filters/index'; -export type MetaFilter = Filter & { +export interface MetaFilter { + filter: Filter; disabled: boolean; negate: boolean; pinned: boolean; index?: string; toElasticsearchQuery: () => void; - applyChanges: (updateObject: Partial) => MetaFilter; -}; +} interface CreateMetaFilterOptions { disabled?: boolean; @@ -40,49 +39,53 @@ export function createMetaFilter( filter: Filter, { disabled = false, negate = false, pinned = false, index }: CreateMetaFilterOptions = {} ): MetaFilter { - const metaFilter = Object.create(filter); - metaFilter.disabled = disabled; - metaFilter.negate = negate; - metaFilter.pinned = pinned; - metaFilter.index = index; - metaFilter.toElasticsearchQuery = function() { - return this; - // TODO implement me - // if negate === true then wrap filter in a `not` filter - // call underlying filter's toElasticsearchQuery - // e.g. Object.getPrototypeOf(this).toElasticsearchQuery(); + return { + filter, + disabled, + negate, + pinned, + index, + toElasticsearchQuery: () => { + // TODO implement me + // if negate === true then wrap filter in a `not` filter + // call underlying filter's toElasticsearchQuery + // e.g. Object.getPrototypeOf(this).toElasticsearchQuery(); + }, }; - metaFilter.applyChanges = function(updateObject: Partial) { - if (isEmpty(updateObject)) { - return this; - } - - const metaProps = ['disabled', 'negate', 'pinned', 'index']; - const currentProps = pick(this, metaProps); - const metaChanges = pick(updateObject, metaProps); - const otherChanges = omit(updateObject, metaProps); - const updatedFilter = Object.getPrototypeOf(this).applyChanges(otherChanges); - const mergedProps = { - ...currentProps, - ...metaChanges, - }; +} - return createMetaFilter(updatedFilter, mergedProps); +export function toggleNegation(metaFilter: MetaFilter) { + const negate = !metaFilter.negate; + return { + ...metaFilter, + negate, }; - return metaFilter; } -export function toggleNegation(filter: MetaFilter) { - const negate = !filter.negate; - return filter.applyChanges({ negate }); +export function togglePinned(metaFilter: MetaFilter) { + const pinned = !metaFilter.pinned; + return { + ...metaFilter, + pinned, + }; } -export function togglePinned(filter: MetaFilter) { - const pinned = !filter.pinned; - return filter.applyChanges({ pinned }); +export function toggleDisabled(metaFilter: MetaFilter) { + const disabled = !metaFilter.disabled; + return { + ...metaFilter, + disabled, + }; } -export function toggleDisabled(filter: MetaFilter) { - const disabled = !filter.disabled; - return filter.applyChanges({ disabled }); +export function updateFilter(metaFilter: MetaFilter, updateObject: Partial) { + const updatedFilter = { + ...metaFilter.filter, + ...updateObject, + }; + + return { + ...metaFilter, + filter: updatedFilter, + }; } diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index fddae6cbda7b9f..6e32a438d2e7d6 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -18,7 +18,6 @@ */ import React, { Component } from 'react'; -import { createFilterBarFilter, FilterBarFilter } from 'ui/filter_bar/filters/filter_bar_filter'; import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; @@ -31,28 +30,27 @@ interface Props { } export class FilterBar extends Component { - public onToggleNegate = (filter: FilterBarFilter) => { - this.props.onToggleNegate(Object.getPrototypeOf(filter)); + public onToggleNegate = (filter: MetaFilter) => { + this.props.onToggleNegate(filter); }; - public onTogglePin = (filter: FilterBarFilter) => { - this.props.onTogglePin(Object.getPrototypeOf(filter)); + public onTogglePin = (filter: MetaFilter) => { + this.props.onTogglePin(filter); }; - public onToggleDisabled = (filter: FilterBarFilter) => { - this.props.onToggleDisabled(Object.getPrototypeOf(filter)); + public onToggleDisabled = (filter: MetaFilter) => { + this.props.onToggleDisabled(filter); }; - public onDelete = (filter: FilterBarFilter) => { - this.props.onDelete(Object.getPrototypeOf(filter)); + public onDelete = (filter: MetaFilter) => { + this.props.onDelete(filter); }; public render() { const filterItems = this.props.filters.map(filter => { - const filterBarFilter = createFilterBarFilter(filter); return ( void; - onToggleNegate: (filter: FilterBarFilter) => void; - onToggleDisabled: (filter: FilterBarFilter) => void; - onDelete: (filter: FilterBarFilter) => void; + onTogglePin: (filter: MetaFilter) => void; + onToggleNegate: (filter: MetaFilter) => void; + onToggleDisabled: (filter: MetaFilter) => void; + onDelete: (filter: MetaFilter) => void; } interface State { @@ -54,11 +55,6 @@ export class FilterItem extends Component { this.props.className ); - let prefix = null; - if (negate) { - prefix = NOT ; - } - const badge = ( { tabIndex: '-1', }} > - {prefix} - {filter.getDisplayText()} + {getFilterDisplayText(filter)} ); @@ -122,7 +117,7 @@ export class FilterItem extends Component { return ( Date: Tue, 13 Nov 2018 16:21:03 -0500 Subject: [PATCH 14/96] further filter view refinements --- .../filter_bar/filters/filter_views/index.ts | 18 +++++++++++------ .../filter_views/phrase_filter_views.tsx | 20 +++++++------------ .../filter_bar/filters/phrase_filter.ts | 17 ---------------- 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/ui/public/filter_bar/filters/filter_views/index.ts b/src/ui/public/filter_bar/filters/filter_views/index.ts index 03349fc1cf5624..e9fd086c5568d5 100644 --- a/src/ui/public/filter_bar/filters/filter_views/index.ts +++ b/src/ui/public/filter_bar/filters/filter_views/index.ts @@ -19,11 +19,8 @@ import { MetaFilter } from 'src/ui/public/filter_bar/filters/meta_filter'; import { Filter } from 'ui/filter_bar/filters'; -import { PhraseFilterViews } from './phrase_filter_views'; - -const filterViews: { [index: string]: FilterViews } = { - PhraseFilter: PhraseFilterViews, -}; +import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; +import { getPhraseFilterViews } from './phrase_filter_views'; export interface FilterViews { getDisplayText: (filter: Filter) => string; @@ -31,6 +28,15 @@ export interface FilterViews { export function getFilterDisplayText(metaFilter: MetaFilter): string { const prefix = metaFilter.negate ? 'NOT ' : ''; - const filterText = filterViews[metaFilter.filter.type].getDisplayText(metaFilter.filter); + const filterText = getViewsForType(metaFilter.filter).getDisplayText(); return `${prefix}${filterText}`; } + +function getViewsForType(filter: Filter) { + switch (filter.type) { + case 'PhraseFilter': + return getPhraseFilterViews(filter as PhraseFilter); + default: + throw new Error(`Unknown type: ${filter.type}`); + } +} diff --git a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx index 208598d4a7d2fe..49e6bc85070212 100644 --- a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx +++ b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx @@ -17,19 +17,13 @@ * under the License. */ -import { Filter } from 'ui/filter_bar/filters'; -import { FilterViews } from 'ui/filter_bar/filters/filter_views/index'; import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; -function getDisplayText(filter: Filter) { - if (filter.type === 'PhraseFilter') { - const { field, value } = filter as PhraseFilter; - return `${field} : ${value}`; - } else { - throw new Error(`${filter.type} is not a PhraseFilter`); - } +export function getPhraseFilterViews(filter: PhraseFilter) { + return { + getDisplayText() { + const { field, value } = filter; + return `${field} : ${value}`; + }, + }; } - -export const PhraseFilterViews: FilterViews = { - getDisplayText, -}; diff --git a/src/ui/public/filter_bar/filters/phrase_filter.ts b/src/ui/public/filter_bar/filters/phrase_filter.ts index 23b80e0614625c..ddb3c4845a54b0 100644 --- a/src/ui/public/filter_bar/filters/phrase_filter.ts +++ b/src/ui/public/filter_bar/filters/phrase_filter.ts @@ -17,13 +17,11 @@ * under the License. */ -import { isEmpty, pick } from 'lodash'; import { Filter } from 'ui/filter_bar/filters/index'; export type PhraseFilter = Filter & { field: string; value: string | number; - applyChanges: (updateObject: Partial) => PhraseFilter; }; interface CreatePhraseFilterOptions { @@ -41,20 +39,5 @@ export function createPhraseFilter(options: CreatePhraseFilterOptions): PhraseFi // TODO implement me return {}; }, - applyChanges(updateObject: Partial) { - if (isEmpty(updateObject)) { - return this; - } - - const props = ['field', 'value']; - const updatedProps = pick(updateObject, props); - const currentProps = pick(this, props); - const mergedProps = { - ...currentProps, - ...updatedProps, - } as CreatePhraseFilterOptions; - - return createPhraseFilter(mergedProps); - }, }; } From a7abc707ab32eb97c5696d28fc5bcc2119ffb421 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 13 Nov 2018 16:23:10 -0500 Subject: [PATCH 15/96] use interface to ensure a filter type supports all required views --- src/ui/public/filter_bar/filters/filter_views/index.ts | 2 +- .../filter_bar/filters/filter_views/phrase_filter_views.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/public/filter_bar/filters/filter_views/index.ts b/src/ui/public/filter_bar/filters/filter_views/index.ts index e9fd086c5568d5..990ebf590b2bdc 100644 --- a/src/ui/public/filter_bar/filters/filter_views/index.ts +++ b/src/ui/public/filter_bar/filters/filter_views/index.ts @@ -23,7 +23,7 @@ import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; import { getPhraseFilterViews } from './phrase_filter_views'; export interface FilterViews { - getDisplayText: (filter: Filter) => string; + getDisplayText: () => string; } export function getFilterDisplayText(metaFilter: MetaFilter): string { diff --git a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx index 49e6bc85070212..476b850de090a8 100644 --- a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx +++ b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx @@ -17,13 +17,13 @@ * under the License. */ +import { FilterViews } from 'ui/filter_bar/filters/filter_views/index'; import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; -export function getPhraseFilterViews(filter: PhraseFilter) { +export function getPhraseFilterViews(filter: PhraseFilter): FilterViews { return { getDisplayText() { - const { field, value } = filter; - return `${field} : ${value}`; + return `${filter.field} : ${filter.value}`; }, }; } From fcd2d6e39cd4529928d409e020e4f36e0f7d17b1 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Wed, 14 Nov 2018 16:03:17 -0500 Subject: [PATCH 16/96] =?UTF-8?q?add=20some=20style=20=F0=9F=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/discover/controllers/discover.js | 7 ++- src/ui/public/_index.scss | 1 + .../react/_global_filter_group.scss | 16 ++++++ .../filter_bar/react/_global_filter_item.scss | 33 +++++++++++ src/ui/public/filter_bar/react/_index.scss | 2 + src/ui/public/filter_bar/react/filter_bar.tsx | 57 ++++++++++++++++--- 6 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 src/ui/public/filter_bar/react/_global_filter_group.scss create mode 100644 src/ui/public/filter_bar/react/_global_filter_item.scss create mode 100644 src/ui/public/filter_bar/react/_index.scss diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index c32922ae6981f3..1d4f7a8f198155 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -190,9 +190,10 @@ function discoverController( $scope.reactFilters = [ - createMetaFilter(createPhraseFilter({ field: 'response', value: 200, index: 'foo' })), - createMetaFilter(createPhraseFilter({ field: 'extension', value: 'jpg', index: 'foo' })), - createMetaFilter(createPhraseFilter({ field: 'bytes', value: 2000, index: 'foo' })), + createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'security', index: 'foo' })), + createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'error', index: 'foo' })), + createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'info', index: 'foo' })), + createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'foo', index: 'foo' })), ]; $scope.onToggleNegate = (filter) => { diff --git a/src/ui/public/_index.scss b/src/ui/public/_index.scss index 234eaa53e96f6f..c6d1c0ef2587bc 100644 --- a/src/ui/public/_index.scss +++ b/src/ui/public/_index.scss @@ -19,6 +19,7 @@ @import './field_editor/index'; @import './notify/index'; @import './query_bar/index'; +@import './filter_bar/react/index'; // The following are prefixed with "vis" diff --git a/src/ui/public/filter_bar/react/_global_filter_group.scss b/src/ui/public/filter_bar/react/_global_filter_group.scss new file mode 100644 index 00000000000000..8d132aca982e09 --- /dev/null +++ b/src/ui/public/filter_bar/react/_global_filter_group.scss @@ -0,0 +1,16 @@ +.globalFilterGroup__filterBar { + margin-top: $euiSizeM; +} + +// sass-lint:disable quotes +.globalFilterGroup__branch { + padding: $euiSize $euiSize $euiSizeS $euiSizeS; + background-repeat: no-repeat; + background-position: right top; + background-image: url("data:image/svg+xml,%0A%3Csvg width='28px' height='28px' viewBox='0 0 28 28' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='#{hexToRGB($euiColorLightShade)}'%3E%3Crect x='14' y='27' width='14' height='1'%3E%3C/rect%3E%3Crect x='0' y='0' width='1' height='14'%3E%3C/rect%3E%3C/g%3E%3C/svg%3E"); +} + +.globalFilterGroup__wrapper { + overflow: hidden; + transition: height $euiAnimSpeedNormal $euiAnimSlightResistance; +} diff --git a/src/ui/public/filter_bar/react/_global_filter_item.scss b/src/ui/public/filter_bar/react/_global_filter_item.scss new file mode 100644 index 00000000000000..daf948b7ce1f20 --- /dev/null +++ b/src/ui/public/filter_bar/react/_global_filter_item.scss @@ -0,0 +1,33 @@ +@import '@elastic/eui/src/components/form/mixins'; +@import '@elastic/eui/src/components/form/variables'; + +.globalFilterItem { + line-height: $euiSizeL + $euiSizeXS; + border: none; + color: $euiTextColor; + + &:not(.globalFilterItem-isDisabled) { + @include euiFormControlDefaultShadow; + } +} + +.globalFilterItem-isDisabled { + background-color: transparentize($euiColorLightShade, .4); + text-decoration: line-through; + font-weight: $euiFontWeightRegular; + font-style: italic; +} + +.globalFilterItem-isPinned { + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: $euiSizeXS; + background-color: $euiColorVis0; + } +} diff --git a/src/ui/public/filter_bar/react/_index.scss b/src/ui/public/filter_bar/react/_index.scss new file mode 100644 index 00000000000000..3c57b7fe2ca3ad --- /dev/null +++ b/src/ui/public/filter_bar/react/_index.scss @@ -0,0 +1,2 @@ +@import 'global_filter_group'; +@import 'global_filter_item'; diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 6e32a438d2e7d6..2e486af3f61d24 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -17,6 +17,8 @@ * under the License. */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; import React, { Component } from 'react'; import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; import { FilterItem } from 'ui/filter_bar/react/filter_item'; @@ -49,16 +51,55 @@ export class FilterBar extends Component { public render() { const filterItems = this.props.filters.map(filter => { return ( - + + + ); }); - return filterItems; + const classes = classNames('globalFilterGroup__wrapper', { + 'globalFilterGroup__wrapper-isVisible': true, + }); + + return ( +
{ + // this.filterBarWrapper = node; + // }} + className={classes} + > +
+ + {/**/} + {/**/} + {/**/} + + + + {filterItems} + + + +
+
+ ); } } From 6e15773ec3ea4cbca5b8d94727742fc6779d0cd0 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Thu, 15 Nov 2018 09:39:00 -0500 Subject: [PATCH 17/96] create search bar component to wrap query bar and filter bar together --- .../public/discover/controllers/discover.js | 2 +- .../kibana/public/discover/index.html | 18 ++-- src/ui/public/filter_bar/react/filter_bar.tsx | 45 +++------ src/ui/public/search_bar/components/index.tsx | 92 +++++++++++++++++++ src/ui/public/search_bar/directive/index.js | 35 +++++++ src/ui/public/search_bar/index.tsx | 22 +++++ 6 files changed, 169 insertions(+), 45 deletions(-) create mode 100644 src/ui/public/search_bar/components/index.tsx create mode 100644 src/ui/public/search_bar/directive/index.js create mode 100644 src/ui/public/search_bar/index.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 1d4f7a8f198155..bd8056e9950d3b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -33,7 +33,7 @@ import 'ui/filters/moment'; import 'ui/index_patterns'; import 'ui/state_management/app_state'; import { timefilter } from 'ui/timefilter'; -import 'ui/query_bar'; +import 'ui/search_bar'; import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; import { toastNotifications } from 'ui/notify'; import { VisProvider } from 'ui/vis'; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index 141b788d8e3a7a..e957a637ef5575 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -28,25 +28,23 @@

- + filters="reactFilters" + on-toggle-filter-negate="onToggleNegate" + on-toggle-filter-pin="onTogglePin" + on-toggle-filter-disabled="onToggleDisabled" + on-filter-delete="onDelete" + >

- void; onTogglePin: (filter: MetaFilter) => void; onDelete: (filter: MetaFilter) => void; + className: string; } export class FilterBar extends Component { @@ -49,6 +50,8 @@ export class FilterBar extends Component { }; public render() { + const classes = classNames('globalFilterBar', this.props.className); + const filterItems = this.props.filters.map(filter => { return ( @@ -63,43 +66,17 @@ export class FilterBar extends Component { ); }); - const classes = classNames('globalFilterGroup__wrapper', { - 'globalFilterGroup__wrapper-isVisible': true, - }); - return ( -
{ - // this.filterBarWrapper = node; - // }} + -
- - {/**/} - {/**/} - {/**/} - - - - {filterItems} - - - -
-
+ {/* TODO display pinned filters first*/} + {filterItems} + ); } } diff --git a/src/ui/public/search_bar/components/index.tsx b/src/ui/public/search_bar/components/index.tsx new file mode 100644 index 00000000000000..95882a53fca560 --- /dev/null +++ b/src/ui/public/search_bar/components/index.tsx @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; +import React, { SFC } from 'react'; +import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; +import { FilterBar } from 'ui/filter_bar/react'; +import { IndexPattern } from 'ui/index_patterns'; +import { QueryBar } from 'ui/query_bar'; +import { Storage } from 'ui/storage'; + +interface Props { + query: { + query: string; + language: string; + }; + onQuerySubmit: (query: { query: string | object; language: string }) => void; + disableAutoFocus?: boolean; + appName: string; + indexPatterns: IndexPattern[]; + store: Storage; + filters: MetaFilter[]; + onToggleFilterNegate: (filter: MetaFilter) => void; + onToggleFilterDisabled: (filter: MetaFilter) => void; + onToggleFilterPin: (filter: MetaFilter) => void; + onFilterDelete: (filter: MetaFilter) => void; +} + +export const SearchBar: SFC = props => { + const classes = classNames('globalFilterGroup__wrapper', { + 'globalFilterGroup__wrapper-isVisible': true, + }); + + return ( +
+ + +
{ this.filterBarWrapper = node; }} + className={classes} + > +
+ + {/**/} + {/**/} + {/**/} + + + + + +
+
+
+ ); +}; diff --git a/src/ui/public/search_bar/directive/index.js b/src/ui/public/search_bar/directive/index.js new file mode 100644 index 00000000000000..d378de81cef1ab --- /dev/null +++ b/src/ui/public/search_bar/directive/index.js @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 'ngreact'; +import { uiModules } from '../../modules'; +import { SearchBar } from '../components'; + +const app = uiModules.get('app/kibana', ['react']); + +app.directive('searchBar', (reactDirective, localStorage) => { + return reactDirective( + SearchBar, + undefined, + {}, + { + store: localStorage, + } + ); +}); diff --git a/src/ui/public/search_bar/index.tsx b/src/ui/public/search_bar/index.tsx new file mode 100644 index 00000000000000..2469f62781f972 --- /dev/null +++ b/src/ui/public/search_bar/index.tsx @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './directive'; + +export { SearchBar } from './components'; From 0ca49118e5d6bb7d6190f939da836e14a0076036 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Thu, 15 Nov 2018 10:37:17 -0500 Subject: [PATCH 18/96] add filter total and visibility toggle button to the search bar --- .../public/query_bar/components/query_bar.tsx | 2 + src/ui/public/search_bar/components/index.tsx | 165 +++++++++++++----- 2 files changed, 122 insertions(+), 45 deletions(-) diff --git a/src/ui/public/query_bar/components/query_bar.tsx b/src/ui/public/query_bar/components/query_bar.tsx index 02f240ffc352b9..95dc359cea6588 100644 --- a/src/ui/public/query_bar/components/query_bar.tsx +++ b/src/ui/public/query_bar/components/query_bar.tsx @@ -74,6 +74,7 @@ interface Props { indexPatterns: IndexPattern[]; store: Storage; intl: InjectedIntl; + prepend: any; } interface State { @@ -510,6 +511,7 @@ export class QueryBarUI extends Component { this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' } role="textbox" + prepend={this.props.prepend} />
void; } -export const SearchBar: SFC = props => { - const classes = classNames('globalFilterGroup__wrapper', { - 'globalFilterGroup__wrapper-isVisible': true, - }); - - return ( -
- - -
{ this.filterBarWrapper = node; }} - className={classes} +interface State { + isFiltersVisible: boolean; +} + +export class SearchBar extends Component { + public filterBarRef: Element | null = null; + public filterBarWrapperRef: Element | null = null; + + public state = { + isFiltersVisible: true, + }; + + public setFilterBarHeight = () => { + requestAnimationFrame(() => { + const height = + this.filterBarRef && this.state.isFiltersVisible ? this.filterBarRef.clientHeight + 4 : 0; + if (this.filterBarWrapperRef) { + this.filterBarWrapperRef.setAttribute('style', `height: ${height}px`); + } + }); + }; + + // member-ordering rules conflict with use-before-declaration rules + /* tslint:disable */ + public ro = new ResizeObserver(this.setFilterBarHeight); + /* tslint:enable */ + + public toggleFiltersVisible = () => { + this.setState({ + isFiltersVisible: !this.state.isFiltersVisible, + }); + }; + + public componentDidMount() { + if (this.filterBarRef) { + this.setFilterBarHeight(); + this.ro.observe(this.filterBarRef); + } + } + + public componentDidUpdate() { + if (this.filterBarRef) { + this.setFilterBarHeight(); + this.ro.unobserve(this.filterBarRef); + } + } + + public render() { + const filterButtonTitle = `${this.props.filters.length} filters applied. Select to ${ + this.state.isFiltersVisible ? 'hide' : 'show' + }.`; + + const filterTriggerButton = ( + 0 ? this.props.filters.length : null} + aria-controls="GlobalFilterGroup" + aria-expanded={!!this.state.isFiltersVisible} + title={filterButtonTitle} > -
- + ); + + const classes = classNames('globalFilterGroup__wrapper', { + 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, + }); + + return ( +
+ + +
{ + this.filterBarWrapperRef = node; + }} + className={classes} + > +
{ + this.filterBarRef = node; + }} > - {/**/} - {/**/} - {/**/} - - - - - + + {/**/} + {/**/} + {/**/} + + + + + +
-
- ); -}; + ); + } +} From 4dad8506762570ca030e9a56e8dd746dd623142b Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Thu, 15 Nov 2018 12:50:41 -0500 Subject: [PATCH 19/96] add "all filters" actions --- .../public/discover/controllers/discover.js | 32 +++- .../kibana/public/discover/index.html | 1 + .../public/filter_bar/filters/meta_filter.ts | 28 +++ .../search_bar/components/filter_options.tsx | 133 ++++++++++++++ src/ui/public/search_bar/components/index.tsx | 149 +-------------- .../search_bar/components/search_bar.tsx | 170 ++++++++++++++++++ 6 files changed, 364 insertions(+), 149 deletions(-) create mode 100644 src/ui/public/search_bar/components/filter_options.tsx create mode 100644 src/ui/public/search_bar/components/search_bar.tsx diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index bd8056e9950d3b..e65383697040b8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -67,7 +67,7 @@ import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_s import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; import { createPhraseFilter } from '../../../../../ui/public/filter_bar/filters/phrase_filter'; import { - createMetaFilter, + createMetaFilter, enable, disable, pin, unpin, toggleDisabled, toggleNegation, togglePinned, } from '../../../../../ui/public/filter_bar/filters/meta_filter'; @@ -215,6 +215,36 @@ function discoverController( $scope.reactFilters = $scope.reactFilters.filter((filter) => filter !== filterToDelete); }; + $scope.onAllFiltersAction = (action) => { + if (action === 'delete') { + $scope.reactFilters = []; + } + else { + $scope.reactFilters.forEach((filter, index) => { + switch (action) { + case 'enable': + $scope.reactFilters[index] = enable(filter); + break; + case 'disable': + $scope.reactFilters[index] = disable(filter); + break; + case 'pin': + $scope.reactFilters[index] = pin(filter); + break; + case 'unpin': + $scope.reactFilters[index] = unpin(filter); + break; + case 'toggleNegate': + $scope.reactFilters[index] = toggleNegation(filter); + break; + case 'toggleDisabled': + $scope.reactFilters[index] = toggleDisabled(filter); + break; + } + }); + } + }; + $scope.getDocLink = getDocLink; $scope.intervalOptions = intervalOptions; $scope.showInterval = false; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index e957a637ef5575..22930c76f1554f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -38,6 +38,7 @@

on-toggle-filter-pin="onTogglePin" on-toggle-filter-disabled="onToggleDisabled" on-filter-delete="onDelete" + on-all-filters-action="onAllFiltersAction" >

diff --git a/src/ui/public/filter_bar/filters/meta_filter.ts b/src/ui/public/filter_bar/filters/meta_filter.ts index 7b6e2e2f1d7da5..894a1d9666cd7f 100644 --- a/src/ui/public/filter_bar/filters/meta_filter.ts +++ b/src/ui/public/filter_bar/filters/meta_filter.ts @@ -54,6 +54,34 @@ export function createMetaFilter( }; } +export function enable(metaFilter: MetaFilter) { + return { + ...metaFilter, + disabled: false, + }; +} + +export function disable(metaFilter: MetaFilter) { + return { + ...metaFilter, + disabled: true, + }; +} + +export function pin(metaFilter: MetaFilter) { + return { + ...metaFilter, + pinned: true, + }; +} + +export function unpin(metaFilter: MetaFilter) { + return { + ...metaFilter, + pinned: false, + }; +} + export function toggleNegation(metaFilter: MetaFilter) { const negate = !metaFilter.negate; return { diff --git a/src/ui/public/search_bar/components/filter_options.tsx b/src/ui/public/search_bar/components/filter_options.tsx new file mode 100644 index 00000000000000..64d773ba886ba6 --- /dev/null +++ b/src/ui/public/search_bar/components/filter_options.tsx @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiButtonIcon, EuiContextMenu, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; +import { Component } from 'react'; +import React from 'react'; + +interface Props { + onAction: (action: string) => void; +} + +interface State { + isPopoverOpen: boolean; +} + +export class FilterOptions extends Component { + public state: State = { + isPopoverOpen: false, + }; + + public togglePopover = () => { + this.setState(prevState => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + public closePopover = () => { + this.setState({ isPopoverOpen: false }); + }; + + public render() { + const panelTree = { + id: 0, + items: [ + { + name: 'Enable all', + icon: 'eye', + onClick: () => { + this.closePopover(); + this.props.onAction('enable'); + }, + }, + { + name: 'Disable all', + icon: 'eyeClosed', + onClick: () => { + this.closePopover(); + this.props.onAction('disable'); + }, + }, + { + name: 'Pin all', + icon: 'pin', + onClick: () => { + this.closePopover(); + this.props.onAction('pin'); + }, + }, + { + name: 'Unpin all', + icon: 'pin', + onClick: () => { + this.closePopover(); + this.props.onAction('unpin'); + }, + }, + { + name: 'Invert inclusion', + icon: 'invert', + onClick: () => { + this.closePopover(); + this.props.onAction('toggleNegate'); + }, + }, + { + name: 'Invert enabled/disabled', + icon: 'eye', + onClick: () => { + this.closePopover(); + this.props.onAction('toggleDisabled'); + }, + }, + { + name: 'Remove all', + icon: 'trash', + onClick: () => { + this.closePopover(); + this.props.onAction('delete'); + }, + }, + ], + }; + + return ( + + } + anchorPosition="downCenter" + panelPaddingSize="none" + withTitle + > + Change all filters + + + ); + } +} diff --git a/src/ui/public/search_bar/components/index.tsx b/src/ui/public/search_bar/components/index.tsx index c91fd0d481fa1f..39410484e9c7fa 100644 --- a/src/ui/public/search_bar/components/index.tsx +++ b/src/ui/public/search_bar/components/index.tsx @@ -17,151 +17,4 @@ * under the License. */ -// @ts-ignore -import { EuiFilterButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import classNames from 'classnames'; -import React, { Component } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; -import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; -import { FilterBar } from 'ui/filter_bar/react'; -import { IndexPattern } from 'ui/index_patterns'; -import { QueryBar } from 'ui/query_bar'; -import { Storage } from 'ui/storage'; - -interface Props { - query: { - query: string; - language: string; - }; - onQuerySubmit: (query: { query: string | object; language: string }) => void; - disableAutoFocus?: boolean; - appName: string; - indexPatterns: IndexPattern[]; - store: Storage; - filters: MetaFilter[]; - onToggleFilterNegate: (filter: MetaFilter) => void; - onToggleFilterDisabled: (filter: MetaFilter) => void; - onToggleFilterPin: (filter: MetaFilter) => void; - onFilterDelete: (filter: MetaFilter) => void; -} - -interface State { - isFiltersVisible: boolean; -} - -export class SearchBar extends Component { - public filterBarRef: Element | null = null; - public filterBarWrapperRef: Element | null = null; - - public state = { - isFiltersVisible: true, - }; - - public setFilterBarHeight = () => { - requestAnimationFrame(() => { - const height = - this.filterBarRef && this.state.isFiltersVisible ? this.filterBarRef.clientHeight + 4 : 0; - if (this.filterBarWrapperRef) { - this.filterBarWrapperRef.setAttribute('style', `height: ${height}px`); - } - }); - }; - - // member-ordering rules conflict with use-before-declaration rules - /* tslint:disable */ - public ro = new ResizeObserver(this.setFilterBarHeight); - /* tslint:enable */ - - public toggleFiltersVisible = () => { - this.setState({ - isFiltersVisible: !this.state.isFiltersVisible, - }); - }; - - public componentDidMount() { - if (this.filterBarRef) { - this.setFilterBarHeight(); - this.ro.observe(this.filterBarRef); - } - } - - public componentDidUpdate() { - if (this.filterBarRef) { - this.setFilterBarHeight(); - this.ro.unobserve(this.filterBarRef); - } - } - - public render() { - const filterButtonTitle = `${this.props.filters.length} filters applied. Select to ${ - this.state.isFiltersVisible ? 'hide' : 'show' - }.`; - - const filterTriggerButton = ( - 0 ? this.props.filters.length : null} - aria-controls="GlobalFilterGroup" - aria-expanded={!!this.state.isFiltersVisible} - title={filterButtonTitle} - > - Filters - - ); - - const classes = classNames('globalFilterGroup__wrapper', { - 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, - }); - - return ( -
- - -
{ - this.filterBarWrapperRef = node; - }} - className={classes} - > -
{ - this.filterBarRef = node; - }} - > - - {/**/} - {/**/} - {/**/} - - - - - -
-
-
- ); - } -} +export { SearchBar } from 'ui/search_bar/components/search_bar'; diff --git a/src/ui/public/search_bar/components/search_bar.tsx b/src/ui/public/search_bar/components/search_bar.tsx new file mode 100644 index 00000000000000..384c90f698da7c --- /dev/null +++ b/src/ui/public/search_bar/components/search_bar.tsx @@ -0,0 +1,170 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// @ts-ignore +import { EuiFilterButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; +import React, { Component } from 'react'; +import ResizeObserver from 'resize-observer-polyfill'; +import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; +import { FilterBar } from 'ui/filter_bar/react'; +import { IndexPattern } from 'ui/index_patterns'; +import { QueryBar } from 'ui/query_bar'; +import { FilterOptions } from 'ui/search_bar/components/filter_options'; +import { Storage } from 'ui/storage'; + +// TODO combine all the filter actions into a single event handler? +interface Props { + query: { + query: string; + language: string; + }; + onQuerySubmit: (query: { query: string | object; language: string }) => void; + disableAutoFocus?: boolean; + appName: string; + indexPatterns: IndexPattern[]; + store: Storage; + filters: MetaFilter[]; + onToggleFilterNegate: (filter: MetaFilter) => void; + onToggleFilterDisabled: (filter: MetaFilter) => void; + onToggleFilterPin: (filter: MetaFilter) => void; + onFilterDelete: (filter: MetaFilter) => void; + onAllFiltersAction: (action: string) => void; +} + +interface State { + isFiltersVisible: boolean; +} + +export class SearchBar extends Component { + public filterBarRef: Element | null = null; + public filterBarWrapperRef: Element | null = null; + + public state = { + isFiltersVisible: true, + }; + + public setFilterBarHeight = () => { + requestAnimationFrame(() => { + const height = + this.filterBarRef && this.state.isFiltersVisible ? this.filterBarRef.clientHeight + 4 : 0; + if (this.filterBarWrapperRef) { + this.filterBarWrapperRef.setAttribute('style', `height: ${height}px`); + } + }); + }; + + // member-ordering rules conflict with use-before-declaration rules + /* tslint:disable */ + public ro = new ResizeObserver(this.setFilterBarHeight); + /* tslint:enable */ + + public toggleFiltersVisible = () => { + this.setState({ + isFiltersVisible: !this.state.isFiltersVisible, + }); + }; + + public componentDidMount() { + if (this.filterBarRef) { + this.setFilterBarHeight(); + this.ro.observe(this.filterBarRef); + } + } + + public componentDidUpdate() { + if (this.filterBarRef) { + this.setFilterBarHeight(); + this.ro.unobserve(this.filterBarRef); + } + } + + public render() { + const filterButtonTitle = `${this.props.filters.length} filters applied. Select to ${ + this.state.isFiltersVisible ? 'hide' : 'show' + }.`; + + const filterTriggerButton = ( + 0 ? this.props.filters.length : null} + aria-controls="GlobalFilterGroup" + aria-expanded={!!this.state.isFiltersVisible} + title={filterButtonTitle} + > + Filters + + ); + + const classes = classNames('globalFilterGroup__wrapper', { + 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, + }); + + return ( +
+ + +
{ + this.filterBarWrapperRef = node; + }} + className={classes} + > +
{ + this.filterBarRef = node; + }} + > + + + + + + + + + +
+
+
+ ); + } +} From 9436a010d3c9cf89cead3fa6da9739aea214046c Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Fri, 16 Nov 2018 16:29:47 -0500 Subject: [PATCH 20/96] foundation for filter editor --- .../filter_bar/react/filter_editor/index.tsx | 28 ++++++ .../public/filter_bar/react/filter_item.tsx | 85 +++++++++++-------- 2 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 src/ui/public/filter_bar/react/filter_editor/index.tsx diff --git a/src/ui/public/filter_bar/react/filter_editor/index.tsx b/src/ui/public/filter_bar/react/filter_editor/index.tsx new file mode 100644 index 00000000000000..5075297bce8631 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/index.tsx @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { SFC } from 'react'; + +interface Props { + foo: string; +} + +export const FilterEditor: SFC = props => { + return
{props.foo}
; +}; diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index d7140e548aa8f1..ca742c6d84f6d6 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -22,6 +22,7 @@ import classNames from 'classnames'; import React, { Component } from 'react'; import { getFilterDisplayText } from 'ui/filter_bar/filters/filter_views'; import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; +import { FilterEditor } from 'ui/filter_bar/react/filter_editor'; interface Props { filter: MetaFilter; @@ -77,43 +78,59 @@ export class FilterItem extends Component { ); - const panelTree = { - id: 0, - items: [ - { - name: `${pinned ? 'Unpin' : 'Pin across all apps'}`, - icon: 'pin', - onClick: () => { - this.closePopover(); - this.props.onTogglePin(filter); + const panelTree = [ + { + id: 0, + items: [ + { + name: `${pinned ? 'Unpin' : 'Pin across all apps'}`, + icon: 'pin', + onClick: () => { + this.closePopover(); + this.props.onTogglePin(filter); + }, + }, + { + name: 'Edit filter', + icon: 'pencil', + panel: 1, }, - }, - { - name: `${negate ? 'Include results' : 'Exclude results'}`, - icon: `${negate ? 'plusInCircle' : 'minusInCircle'}`, - onClick: () => { - this.closePopover(); - this.props.onToggleNegate(filter); + { + name: `${negate ? 'Include results' : 'Exclude results'}`, + icon: `${negate ? 'plusInCircle' : 'minusInCircle'}`, + onClick: () => { + this.closePopover(); + this.props.onToggleNegate(filter); + }, }, - }, - { - name: `${disabled ? 'Re-enable' : 'Temporarily disable'}`, - icon: `${disabled ? 'eye' : 'eyeClosed'}`, - onClick: () => { - this.closePopover(); - this.props.onToggleDisabled(filter); + { + name: `${disabled ? 'Re-enable' : 'Temporarily disable'}`, + icon: `${disabled ? 'eye' : 'eyeClosed'}`, + onClick: () => { + this.closePopover(); + this.props.onToggleDisabled(filter); + }, }, - }, - { - name: 'Delete', - icon: 'trash', - onClick: () => { - this.closePopover(); - this.props.onDelete(filter); + { + name: 'Delete', + icon: 'trash', + onClick: () => { + this.closePopover(); + this.props.onDelete(filter); + }, }, - }, - ], - }; + ], + }, + { + id: 1, + width: 400, + content: ( +
+ +
+ ), + }, + ]; return ( { anchorPosition="downCenter" panelPaddingSize="none" > - + ); } From 967f97f2eb8ea9e1ea234b27bd8be9c2094156d3 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Fri, 16 Nov 2018 17:15:58 -0500 Subject: [PATCH 21/96] recreated filter editor mockup in TS as a starting point --- .../filter_bar/react/filter_editor/index.tsx | 258 +++++++++++++++++- 1 file changed, 254 insertions(+), 4 deletions(-) diff --git a/src/ui/public/filter_bar/react/filter_editor/index.tsx b/src/ui/public/filter_bar/react/filter_editor/index.tsx index 5075297bce8631..277e0a3b517c65 100644 --- a/src/ui/public/filter_bar/react/filter_editor/index.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/index.tsx @@ -17,12 +17,262 @@ * under the License. */ -import React, { SFC } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiComboBox, + EuiComboBoxOptionProps, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import React, { Component } from 'react'; +import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; + +const fieldOptions = [ + { + label: 'Fields', + isGroupLabelOption: true, + }, + { + label: 'field_1', + }, + { + label: 'field_2', + }, + { + label: 'field_3', + }, + { + label: 'field_4', + }, +]; +const operatorOptions = [ + { + label: 'Operators', + isGroupLabelOption: true, + }, + { + label: 'IS', + }, + { + label: 'IS NOT', + }, + { + label: 'IS ONE OF', + }, + { + label: 'EXISTS', + }, +]; +const valueOptions = [ + { + label: 'Values', + isGroupLabelOption: true, + }, + { + label: 'Value 1', + }, + { + label: 'Value 2', + }, + { + label: 'Value 3', + }, + { + label: 'Value 4', + }, +]; interface Props { foo: string; + filter: MetaFilter; +} + +interface State { + selectedField: EuiComboBoxOptionProps[]; + selectedOperand: EuiComboBoxOptionProps[]; + selectedValues: EuiComboBoxOptionProps[]; + valueOptions: EuiComboBoxOptionProps[]; + operatorOptions: EuiComboBoxOptionProps[]; + fieldOptions: EuiComboBoxOptionProps[]; + useCustomLabel: boolean; + customLabel: string | null; } -export const FilterEditor: SFC = props => { - return
{props.foo}
; -}; +export class FilterEditor extends Component { + public state = { + fieldOptions, + operatorOptions, + valueOptions, + selectedField: [], + selectedOperand: [], + selectedValues: [], + useCustomLabel: false, + customLabel: null, + }; + + public onFieldChange = (selectedOptions: EuiComboBoxOptionProps[]) => { + // We should only get back either 0 or 1 options. + this.setState({ + selectedField: selectedOptions, + }); + }; + + public onOperandChange = (selectedOptions: EuiComboBoxOptionProps[]) => { + // We should only get back either 0 or 1 options. + this.setState({ + selectedOperand: selectedOptions, + }); + }; + + public onValuesChange = (selectedOptions: EuiComboBoxOptionProps[]) => { + this.setState({ + selectedValues: selectedOptions, + }); + }; + + public onCustomLabelSwitchChange = (event: React.ChangeEvent) => { + this.setState({ + useCustomLabel: event.target.checked, + }); + }; + + public onFieldSearchChange = (searchValue: string) => { + this.setState({ + fieldOptions: fieldOptions.filter(option => + option.label.toLowerCase().includes(searchValue.toLowerCase()) + ), + }); + }; + + public onOperandSearchChange = (searchValue: string) => { + this.setState({ + operatorOptions: operatorOptions.filter(option => + option.label.toLowerCase().includes(searchValue.toLowerCase()) + ), + }); + }; + + public onValuesSearchChange = (searchValue: string) => { + this.setState({ + valueOptions: valueOptions.filter(option => + option.label.toLowerCase().includes(searchValue.toLowerCase()) + ), + }); + }; + + public resetForm = () => { + this.setState({ + selectedField: [], + selectedOperand: [], + selectedValues: [], + useCustomLabel: false, + customLabel: null, + }); + }; + + public render() { + return ( +
+ + + + + + + + + + + + + + + +
+ + + +
+ + + + + + {this.state.useCustomLabel && ( +
+ + + {}} /> + +
+ )} + + + + + + {}}> + Add + + + + {} : this.resetForm}> + {this.props.filter ? 'Cancel' : 'Reset form'} + + + + + {this.props.filter && ( + + Delete + + )} + + +
+ ); + } +} From cc68d0ea28a9a2c43ce5e9fe345f7aa25788a1ec Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 4 Dec 2018 13:45:07 -0700 Subject: [PATCH 22/96] Fix import path --- .../kibana/public/discover/controllers/discover.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index e65383697040b8..e433e2c5c86b27 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -65,12 +65,12 @@ import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; -import { createPhraseFilter } from '../../../../../ui/public/filter_bar/filters/phrase_filter'; +import { createPhraseFilter } from '../../../../../../ui/public/filter_bar/filters/phrase_filter'; import { createMetaFilter, enable, disable, pin, unpin, toggleDisabled, toggleNegation, togglePinned, -} from '../../../../../ui/public/filter_bar/filters/meta_filter'; +} from '../../../../../../ui/public/filter_bar/filters/meta_filter'; const app = uiModules.get('apps/discover', [ 'kibana/notify', From e360cce2bebf2f8d815e192bf0912151052d4502 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 5 Dec 2018 14:26:12 -0700 Subject: [PATCH 23/96] Fix unresolved merge conflict --- .../kibana/public/discover/index.html | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index 22930c76f1554f..61e5b0e9ebe118 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -96,16 +96,11 @@

ng-hide="fetchError" class="dscOverlay" > -<<<<<<< HEAD:src/legacy/core_plugins/kibana/public/discover/index.html -
+

-======= -
-

Searching

->>>>>>> wire up the callbacks for the FilterItem:src/core_plugins/kibana/public/discover/index.html
{{fetchStatus.complete}}/{{fetchStatus.total}}
@@ -170,15 +165,9 @@

Searching

-======= - ng-if="vis && rows.length !== 0" - style="display: flex; height: 200px" + ng-show="vis && rows.length !== 0" + style="display: flex; height: 200px" > ->>>>>>> wire up the callbacks for the FilterItem:src/core_plugins/kibana/public/discover/index.html
From ebbde92c8fc0725e394fa13050219aef8411c170 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Thu, 6 Dec 2018 14:55:30 -0700 Subject: [PATCH 24/96] Fix injecting i18n provider --- src/ui/public/filter_bar/react/filter_bar.tsx | 4 ++-- src/ui/public/search_bar/directive/index.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index a4904a2d49b9c6..11912834ec42df 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -52,9 +52,9 @@ export class FilterBar extends Component { public render() { const classes = classNames('globalFilterBar', this.props.className); - const filterItems = this.props.filters.map(filter => { + const filterItems = this.props.filters.map((filter, i) => { return ( - + { return reactDirective( - SearchBar, + injectI18nProvider(SearchBar), undefined, {}, { From 692604d22272392ccaa30e6971ffe21474b0038d Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 19 Dec 2018 10:02:12 -0700 Subject: [PATCH 25/96] Hook up search bar to actual filters --- .../public/discover/controllers/discover.js | 71 ++-------- .../kibana/public/discover/index.html | 8 +- .../filter_bar/filters/custom_filter.ts | 24 ++++ .../filter_bar/filters/exists_filter.ts | 28 ++++ .../filter_views/custom_filter_views.tsx | 29 ++++ .../filter_views/exists_filter_views.tsx | 29 ++++ .../geo_bounding_box_filter_views.tsx | 32 +++++ .../filter_views/geo_polygon_filter_views.tsx | 32 +++++ .../filter_bar/filters/filter_views/index.ts | 50 +++++-- .../filter_views/phrase_filter_views.tsx | 6 +- .../filter_views/phrases_filter_views.tsx | 29 ++++ .../filter_views/query_filter_views.tsx | 29 ++++ .../filter_views/range_filter_views.tsx | 50 +++++++ .../filters/geo_bounding_box_filter.ts | 32 +++++ .../filter_bar/filters/geo_polygon_filter.ts | 31 +++++ src/ui/public/filter_bar/filters/index.ts | 28 +++- .../public/filter_bar/filters/meta_filter.ts | 117 ++++++---------- .../filter_bar/filters/phrase_filter.ts | 31 ++--- .../filter_bar/filters/phrases_filter.ts | 30 ++++ .../public/filter_bar/filters/query_filter.ts | 28 ++++ .../public/filter_bar/filters/range_filter.ts | 34 +++++ src/ui/public/filter_bar/query_filter.js | 10 ++ src/ui/public/filter_bar/react/filter_bar.tsx | 129 ++++++++++++++---- .../filter_bar/react/filter_editor/index.tsx | 10 +- .../public/filter_bar/react/filter_item.tsx | 30 ++-- .../search_bar/components/filter_options.tsx | 22 +-- .../search_bar/components/search_bar.tsx | 33 +---- 27 files changed, 720 insertions(+), 262 deletions(-) create mode 100644 src/ui/public/filter_bar/filters/custom_filter.ts create mode 100644 src/ui/public/filter_bar/filters/exists_filter.ts create mode 100644 src/ui/public/filter_bar/filters/filter_views/custom_filter_views.tsx create mode 100644 src/ui/public/filter_bar/filters/filter_views/exists_filter_views.tsx create mode 100644 src/ui/public/filter_bar/filters/filter_views/geo_bounding_box_filter_views.tsx create mode 100644 src/ui/public/filter_bar/filters/filter_views/geo_polygon_filter_views.tsx create mode 100644 src/ui/public/filter_bar/filters/filter_views/phrases_filter_views.tsx create mode 100644 src/ui/public/filter_bar/filters/filter_views/query_filter_views.tsx create mode 100644 src/ui/public/filter_bar/filters/filter_views/range_filter_views.tsx create mode 100644 src/ui/public/filter_bar/filters/geo_bounding_box_filter.ts create mode 100644 src/ui/public/filter_bar/filters/geo_polygon_filter.ts create mode 100644 src/ui/public/filter_bar/filters/phrases_filter.ts create mode 100644 src/ui/public/filter_bar/filters/query_filter.ts create mode 100644 src/ui/public/filter_bar/filters/range_filter.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index e433e2c5c86b27..55a3ae7fb5c210 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -65,12 +65,6 @@ import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; -import { createPhraseFilter } from '../../../../../../ui/public/filter_bar/filters/phrase_filter'; -import { - createMetaFilter, enable, disable, pin, unpin, - toggleDisabled, - toggleNegation, togglePinned, -} from '../../../../../../ui/public/filter_bar/filters/meta_filter'; const app = uiModules.get('apps/discover', [ 'kibana/notify', @@ -188,63 +182,6 @@ function discoverController( requests: new RequestAdapter() }; - - $scope.reactFilters = [ - createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'security', index: 'foo' })), - createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'error', index: 'foo' })), - createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'info', index: 'foo' })), - createMetaFilter(createPhraseFilter({ field: '@tags.keyword', value: 'foo', index: 'foo' })), - ]; - - $scope.onToggleNegate = (filter) => { - const index = $scope.reactFilters.indexOf(filter); - $scope.reactFilters[index] = toggleNegation(filter); - }; - - $scope.onTogglePin = (filter) => { - const index = $scope.reactFilters.indexOf(filter); - $scope.reactFilters[index] = togglePinned(filter); - }; - - $scope.onToggleDisabled = (filter) => { - const index = $scope.reactFilters.indexOf(filter); - $scope.reactFilters[index] = toggleDisabled(filter); - }; - - $scope.onDelete = (filterToDelete) => { - $scope.reactFilters = $scope.reactFilters.filter((filter) => filter !== filterToDelete); - }; - - $scope.onAllFiltersAction = (action) => { - if (action === 'delete') { - $scope.reactFilters = []; - } - else { - $scope.reactFilters.forEach((filter, index) => { - switch (action) { - case 'enable': - $scope.reactFilters[index] = enable(filter); - break; - case 'disable': - $scope.reactFilters[index] = disable(filter); - break; - case 'pin': - $scope.reactFilters[index] = pin(filter); - break; - case 'unpin': - $scope.reactFilters[index] = unpin(filter); - break; - case 'toggleNegate': - $scope.reactFilters[index] = toggleNegation(filter); - break; - case 'toggleDisabled': - $scope.reactFilters[index] = toggleDisabled(filter); - break; - } - }); - } - }; - $scope.getDocLink = getDocLink; $scope.intervalOptions = intervalOptions; $scope.showInterval = false; @@ -413,6 +350,13 @@ function discoverController( const $state = $scope.state = new AppState(getStateDefaults()); + $scope.filters = queryFilter.getFilters(); + + $scope.onFiltersUpdated = filters => { + // The filters will automatically be set when the queryFilter emits an update event (see below) + queryFilter.setFilters(filters); + }; + const getFieldCounts = async () => { // the field counts aren't set until we have the data back, // so we wait for the fetch to be done before proceeding @@ -549,6 +493,7 @@ function discoverController( // update data source when filters update $scope.$listen(queryFilter, 'update', function () { + $scope.filters = queryFilter.getFilters(); return $scope.updateDataSource().then(function () { $state.save(); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index 61e5b0e9ebe118..d60227d1a9259c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -33,12 +33,8 @@

on-query-submit="updateQueryAndFetch" app-name="'discover'" index-patterns="[indexPattern]" - filters="reactFilters" - on-toggle-filter-negate="onToggleNegate" - on-toggle-filter-pin="onTogglePin" - on-toggle-filter-disabled="onToggleDisabled" - on-filter-delete="onDelete" - on-all-filters-action="onAllFiltersAction" + filters="filters" + on-filters-updated="onFiltersUpdated" >

diff --git a/src/ui/public/filter_bar/filters/custom_filter.ts b/src/ui/public/filter_bar/filters/custom_filter.ts new file mode 100644 index 00000000000000..e15cc31e7e9d9c --- /dev/null +++ b/src/ui/public/filter_bar/filters/custom_filter.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MetaFilter } from './meta_filter'; + +export type CustomFilter = MetaFilter & { + query: any; +}; diff --git a/src/ui/public/filter_bar/filters/exists_filter.ts b/src/ui/public/filter_bar/filters/exists_filter.ts new file mode 100644 index 00000000000000..6d31916ea5f0f0 --- /dev/null +++ b/src/ui/public/filter_bar/filters/exists_filter.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FilterMeta, MetaFilter } from './meta_filter'; + +export type ExistsFilterMeta = FilterMeta & { + key: string; // The name of the field +}; + +export type ExistsFilter = MetaFilter & { + meta: ExistsFilterMeta; +}; diff --git a/src/ui/public/filter_bar/filters/filter_views/custom_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/custom_filter_views.tsx new file mode 100644 index 00000000000000..eb01e75318bf04 --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/custom_filter_views.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CustomFilter } from '../custom_filter'; +import { FilterViews } from './index'; + +export function getCustomFilterViews(filter: CustomFilter): FilterViews { + return { + getDisplayText() { + return JSON.stringify(filter.query); + }, + }; +} diff --git a/src/ui/public/filter_bar/filters/filter_views/exists_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/exists_filter_views.tsx new file mode 100644 index 00000000000000..6c60b39e47cb5f --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/exists_filter_views.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExistsFilter } from '../exists_filter'; +import { FilterViews } from './index'; + +export function getExistsFilterViews(filter: ExistsFilter): FilterViews { + return { + getDisplayText() { + return `${filter.meta.key} exists`; + }, + }; +} diff --git a/src/ui/public/filter_bar/filters/filter_views/geo_bounding_box_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/geo_bounding_box_filter_views.tsx new file mode 100644 index 00000000000000..8057caa504e1ad --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/geo_bounding_box_filter_views.tsx @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GeoBoundingBoxFilter } from '../geo_bounding_box_filter'; +import { FilterViews } from './index'; + +export function getGeoBoundingBoxFilterViews(filter: GeoBoundingBoxFilter): FilterViews { + return { + getDisplayText() { + const { meta } = filter; + const { key, params } = meta; + const { bottom_right: bottomRight, top_left: topLeft } = params; + return `${key}: ${JSON.stringify(topLeft)} to ${JSON.stringify(bottomRight)}`; + }, + }; +} diff --git a/src/ui/public/filter_bar/filters/filter_views/geo_polygon_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/geo_polygon_filter_views.tsx new file mode 100644 index 00000000000000..507116a0a11878 --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/geo_polygon_filter_views.tsx @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { GeoPolygonFilter } from '../geo_polygon_filter'; +import { FilterViews } from './index'; + +export function getGeoPolygonFilterViews(filter: GeoPolygonFilter): FilterViews { + return { + getDisplayText() { + const { meta } = filter; + const { key, params } = meta; + const { points } = params; + return `${key}: ${points.map(point => JSON.stringify(point)).join(', ')}`; + }, + }; +} diff --git a/src/ui/public/filter_bar/filters/filter_views/index.ts b/src/ui/public/filter_bar/filters/filter_views/index.ts index 990ebf590b2bdc..48098458c8f3c3 100644 --- a/src/ui/public/filter_bar/filters/filter_views/index.ts +++ b/src/ui/public/filter_bar/filters/filter_views/index.ts @@ -17,26 +17,56 @@ * under the License. */ -import { MetaFilter } from 'src/ui/public/filter_bar/filters/meta_filter'; -import { Filter } from 'ui/filter_bar/filters'; -import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; +import { + CustomFilter, + ExistsFilter, + GeoBoundingBoxFilter, + GeoPolygonFilter, + MetaFilter, + PhraseFilter, + PhrasesFilter, + QueryFilter, + RangeFilter, +} from '../index'; +import { getCustomFilterViews } from './custom_filter_views'; +import { getExistsFilterViews } from './exists_filter_views'; +import { getGeoBoundingBoxFilterViews } from './geo_bounding_box_filter_views'; +import { getGeoPolygonFilterViews } from './geo_polygon_filter_views'; import { getPhraseFilterViews } from './phrase_filter_views'; +import { getPhrasesFilterViews } from './phrases_filter_views'; +import { getQueryFilterViews } from './query_filter_views'; +import { getRangeFilterViews } from './range_filter_views'; export interface FilterViews { getDisplayText: () => string; } -export function getFilterDisplayText(metaFilter: MetaFilter): string { - const prefix = metaFilter.negate ? 'NOT ' : ''; - const filterText = getViewsForType(metaFilter.filter).getDisplayText(); +export function getFilterDisplayText(filter: MetaFilter): string { + if (filter.meta.alias !== null) { + return filter.meta.alias; + } + const prefix = filter.meta.negate ? 'NOT ' : ''; + const filterText = getViewsForType(filter).getDisplayText(); return `${prefix}${filterText}`; } -function getViewsForType(filter: Filter) { - switch (filter.type) { - case 'PhraseFilter': +function getViewsForType(filter: MetaFilter) { + switch (filter.meta.type) { + case 'exists': + return getExistsFilterViews(filter as ExistsFilter); + case 'geo_bounding_box': + return getGeoBoundingBoxFilterViews(filter as GeoBoundingBoxFilter); + case 'geo_polygon': + return getGeoPolygonFilterViews(filter as GeoPolygonFilter); + case 'phrase': return getPhraseFilterViews(filter as PhraseFilter); + case 'phrases': + return getPhrasesFilterViews(filter as PhrasesFilter); + case 'query_string': + return getQueryFilterViews(filter as QueryFilter); + case 'range': + return getRangeFilterViews(filter as RangeFilter); default: - throw new Error(`Unknown type: ${filter.type}`); + return getCustomFilterViews(filter as CustomFilter); } } diff --git a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx index 476b850de090a8..9e258048faa510 100644 --- a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx +++ b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx @@ -17,13 +17,13 @@ * under the License. */ -import { FilterViews } from 'ui/filter_bar/filters/filter_views/index'; -import { PhraseFilter } from 'ui/filter_bar/filters/phrase_filter'; +import { PhraseFilter } from '../phrase_filter'; +import { FilterViews } from './index'; export function getPhraseFilterViews(filter: PhraseFilter): FilterViews { return { getDisplayText() { - return `${filter.field} : ${filter.value}`; + return `${filter.meta.key} : ${filter.meta.value}`; }, }; } diff --git a/src/ui/public/filter_bar/filters/filter_views/phrases_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/phrases_filter_views.tsx new file mode 100644 index 00000000000000..e3def7acfa8d8b --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/phrases_filter_views.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PhrasesFilter } from '../phrases_filter'; +import { FilterViews } from './index'; + +export function getPhrasesFilterViews(filter: PhrasesFilter): FilterViews { + return { + getDisplayText() { + return `${filter.meta.key} is one of ${filter.meta.value}`; + }, + }; +} diff --git a/src/ui/public/filter_bar/filters/filter_views/query_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/query_filter_views.tsx new file mode 100644 index 00000000000000..297ae891d39205 --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/query_filter_views.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { QueryFilter } from '../query_filter'; +import { FilterViews } from './index'; + +export function getQueryFilterViews(filter: QueryFilter): FilterViews { + return { + getDisplayText() { + return filter.meta.value; + }, + }; +} diff --git a/src/ui/public/filter_bar/filters/filter_views/range_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/range_filter_views.tsx new file mode 100644 index 00000000000000..3a19120006b010 --- /dev/null +++ b/src/ui/public/filter_bar/filters/filter_views/range_filter_views.tsx @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { RangeFilter } from '../range_filter'; +import { FilterViews } from './index'; + +export function getRangeFilterViews(filter: RangeFilter): FilterViews { + return { + getDisplayText() { + const { meta } = filter; + const { key, params } = meta; + const { gt, gte, lt, lte } = params; + return `${key}: ${getFrom(gt, gte)} to ${getTo(lt, lte)}`; + }, + }; +} + +function getFrom(gt: string | number | undefined, gte: string | number | undefined): string { + if (typeof gt !== 'undefined' && gt !== null) { + return `${gt}`; + } else if (typeof gte !== 'undefined' && gte !== null) { + return `${gte}`; + } + return '-∞'; +} + +function getTo(lt: string | number | undefined, lte: string | number | undefined): string { + if (typeof lt !== 'undefined' && lt !== null) { + return `${lt}`; + } else if (typeof lte !== 'undefined' && lte !== null) { + return `${lte}`; + } + return '∞'; +} diff --git a/src/ui/public/filter_bar/filters/geo_bounding_box_filter.ts b/src/ui/public/filter_bar/filters/geo_bounding_box_filter.ts new file mode 100644 index 00000000000000..fd6cae7a650d97 --- /dev/null +++ b/src/ui/public/filter_bar/filters/geo_bounding_box_filter.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FilterMeta, LatLon, MetaFilter } from './meta_filter'; + +export type GeoBoundingBoxFilterMeta = FilterMeta & { + key: string; // The name of the field + params: { + bottom_right: LatLon; + top_left: LatLon; + }; +}; + +export type GeoBoundingBoxFilter = MetaFilter & { + meta: GeoBoundingBoxFilterMeta; +}; diff --git a/src/ui/public/filter_bar/filters/geo_polygon_filter.ts b/src/ui/public/filter_bar/filters/geo_polygon_filter.ts new file mode 100644 index 00000000000000..b71effdabc74aa --- /dev/null +++ b/src/ui/public/filter_bar/filters/geo_polygon_filter.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FilterMeta, LatLon, MetaFilter } from './meta_filter'; + +export type GeoPolygonFilterMeta = FilterMeta & { + key: string; // The name of the field + params: { + points: LatLon[]; + }; +}; + +export type GeoPolygonFilter = MetaFilter & { + meta: GeoPolygonFilterMeta; +}; diff --git a/src/ui/public/filter_bar/filters/index.ts b/src/ui/public/filter_bar/filters/index.ts index 7dc64c1fa6a228..cc68b65c5d9e2f 100644 --- a/src/ui/public/filter_bar/filters/index.ts +++ b/src/ui/public/filter_bar/filters/index.ts @@ -17,7 +17,27 @@ * under the License. */ -export interface Filter { - type: string; - toElasticsearchQuery: () => any; -} +// The interface the other filters extend +export { MetaFilter } from './meta_filter'; + +// Helper functions that can be invoked on any filter +export { + isFilterPinned, + toggleFilterDisabled, + toggleFilterNegated, + toggleFilterPinned, + enableFilter, + disableFilter, + pinFilter, + unpinFilter, +} from './meta_filter'; + +// The actual filter types +export { CustomFilter } from './custom_filter'; +export { ExistsFilter } from './exists_filter'; +export { GeoBoundingBoxFilter } from './geo_bounding_box_filter'; +export { GeoPolygonFilter } from './geo_polygon_filter'; +export { PhraseFilter } from './phrase_filter'; +export { PhrasesFilter } from './phrases_filter'; +export { QueryFilter } from './query_filter'; +export { RangeFilter } from './range_filter'; diff --git a/src/ui/public/filter_bar/filters/meta_filter.ts b/src/ui/public/filter_bar/filters/meta_filter.ts index 894a1d9666cd7f..5477e498bac17c 100644 --- a/src/ui/public/filter_bar/filters/meta_filter.ts +++ b/src/ui/public/filter_bar/filters/meta_filter.ts @@ -17,103 +17,68 @@ * under the License. */ -import { Filter } from 'ui/filter_bar/filters/index'; +export enum FilterStateStore { + APP_STATE = 'appState', + GLOBAL_STATE = 'globalState', +} -export interface MetaFilter { - filter: Filter; +export interface FilterState { + store: FilterStateStore; +} + +export interface FilterMeta { + type: string; disabled: boolean; negate: boolean; - pinned: boolean; - index?: string; - toElasticsearchQuery: () => void; + alias: string | null; + index: string; } -interface CreateMetaFilterOptions { - disabled?: boolean; - negate?: boolean; - pinned?: boolean; - index?: string; +export interface MetaFilter { + $state: FilterState; + meta: FilterMeta; + query?: any; } -export function createMetaFilter( - filter: Filter, - { disabled = false, negate = false, pinned = false, index }: CreateMetaFilterOptions = {} -): MetaFilter { - return { - filter, - disabled, - negate, - pinned, - index, - toElasticsearchQuery: () => { - // TODO implement me - // if negate === true then wrap filter in a `not` filter - // call underlying filter's toElasticsearchQuery - // e.g. Object.getPrototypeOf(this).toElasticsearchQuery(); - }, - }; +export interface LatLon { + lat: number; + lon: number; } -export function enable(metaFilter: MetaFilter) { - return { - ...metaFilter, - disabled: false, - }; +export function isFilterPinned(filter: MetaFilter) { + return filter.$state.store === FilterStateStore.GLOBAL_STATE; } -export function disable(metaFilter: MetaFilter) { - return { - ...metaFilter, - disabled: true, - }; +export function toggleFilterDisabled(filter: MetaFilter) { + const disabled = !filter.meta.disabled; + const meta = { ...filter.meta, disabled }; + return { ...filter, meta }; } -export function pin(metaFilter: MetaFilter) { - return { - ...metaFilter, - pinned: true, - }; +export function toggleFilterNegated(filter: MetaFilter) { + const negate = !filter.meta.negate; + const meta = { ...filter.meta, negate }; + return { ...filter, meta }; } -export function unpin(metaFilter: MetaFilter) { - return { - ...metaFilter, - pinned: false, - }; +export function toggleFilterPinned(filter: MetaFilter) { + const store = isFilterPinned(filter) ? FilterStateStore.APP_STATE : FilterStateStore.GLOBAL_STATE; + const $state = { ...filter.$state, store }; + return { ...filter, $state }; } -export function toggleNegation(metaFilter: MetaFilter) { - const negate = !metaFilter.negate; - return { - ...metaFilter, - negate, - }; +export function enableFilter(filter: MetaFilter) { + return !filter.meta.disabled ? filter : toggleFilterDisabled(filter); } -export function togglePinned(metaFilter: MetaFilter) { - const pinned = !metaFilter.pinned; - return { - ...metaFilter, - pinned, - }; +export function disableFilter(filter: MetaFilter) { + return filter.meta.disabled ? filter : toggleFilterDisabled(filter); } -export function toggleDisabled(metaFilter: MetaFilter) { - const disabled = !metaFilter.disabled; - return { - ...metaFilter, - disabled, - }; +export function pinFilter(filter: MetaFilter) { + return isFilterPinned(filter) ? filter : toggleFilterPinned(filter); } -export function updateFilter(metaFilter: MetaFilter, updateObject: Partial) { - const updatedFilter = { - ...metaFilter.filter, - ...updateObject, - }; - - return { - ...metaFilter, - filter: updatedFilter, - }; +export function unpinFilter(filter: MetaFilter) { + return !isFilterPinned(filter) ? filter : toggleFilterPinned(filter); } diff --git a/src/ui/public/filter_bar/filters/phrase_filter.ts b/src/ui/public/filter_bar/filters/phrase_filter.ts index ddb3c4845a54b0..30520bfe14a1df 100644 --- a/src/ui/public/filter_bar/filters/phrase_filter.ts +++ b/src/ui/public/filter_bar/filters/phrase_filter.ts @@ -17,27 +17,16 @@ * under the License. */ -import { Filter } from 'ui/filter_bar/filters/index'; +import { FilterMeta, MetaFilter } from './meta_filter'; -export type PhraseFilter = Filter & { - field: string; - value: string | number; +export type PhraseFilterMeta = FilterMeta & { + key: string; // The name of the field + value: string; // The formatted value + params: { + query: string; // The unformatted value + }; }; -interface CreatePhraseFilterOptions { - field: string; - value: string | number; -} - -export function createPhraseFilter(options: CreatePhraseFilterOptions): PhraseFilter { - const { field, value } = options; - return { - type: 'PhraseFilter', - field, - value, - toElasticsearchQuery() { - // TODO implement me - return {}; - }, - }; -} +export type PhraseFilter = MetaFilter & { + meta: PhraseFilterMeta; +}; diff --git a/src/ui/public/filter_bar/filters/phrases_filter.ts b/src/ui/public/filter_bar/filters/phrases_filter.ts new file mode 100644 index 00000000000000..8ff813d976e111 --- /dev/null +++ b/src/ui/public/filter_bar/filters/phrases_filter.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FilterMeta, MetaFilter } from './meta_filter'; + +export type PhrasesFilterMeta = FilterMeta & { + key: string; // The name of the field + params: string[]; // The unformatted values + value: string; // The formatted values concatenated together +}; + +export type PhrasesFilter = MetaFilter & { + meta: PhrasesFilterMeta; +}; diff --git a/src/ui/public/filter_bar/filters/query_filter.ts b/src/ui/public/filter_bar/filters/query_filter.ts new file mode 100644 index 00000000000000..400c5d28dedd36 --- /dev/null +++ b/src/ui/public/filter_bar/filters/query_filter.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FilterMeta, MetaFilter } from './meta_filter'; + +export type QueryFilterMeta = FilterMeta & { + value: string; // The query string +}; + +export type QueryFilter = MetaFilter & { + meta: QueryFilterMeta; +}; diff --git a/src/ui/public/filter_bar/filters/range_filter.ts b/src/ui/public/filter_bar/filters/range_filter.ts new file mode 100644 index 00000000000000..9951aff8c19a96 --- /dev/null +++ b/src/ui/public/filter_bar/filters/range_filter.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FilterMeta, MetaFilter } from './meta_filter'; + +export type RangeFilterMeta = FilterMeta & { + key: string; // The name of the field + params: { + gt?: number | string; + gte?: number | string; + lte?: number | string; + lt?: number | string; + }; +}; + +export type RangeFilter = MetaFilter & { + meta: RangeFilterMeta; +}; diff --git a/src/ui/public/filter_bar/query_filter.js b/src/ui/public/filter_bar/query_filter.js index 0c1e68c084535e..8a7133e3d486cc 100644 --- a/src/ui/public/filter_bar/query_filter.js +++ b/src/ui/public/filter_bar/query_filter.js @@ -216,6 +216,16 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g executeOnFilters(pin); }; + queryFilter.setFilters = filters => { + const appState = getAppState(); + const [globalFilters, appFilters] = _.partition(filters, filter => { + return filter.$state.store === 'globalState'; + }); + globalState.filters = globalFilters; + if (appState) appState.filters = appFilters; + saveState(); + }; + initWatchers(); return queryFilter; diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index 11912834ec42df..cdb0a69036fd23 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -20,33 +20,93 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import classNames from 'classnames'; import React, { Component } from 'react'; -import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; -import { FilterItem } from 'ui/filter_bar/react/filter_item'; +import { FilterOptions } from 'ui/search_bar/components/filter_options'; +import { + disableFilter, + enableFilter, + MetaFilter, + pinFilter, + toggleFilterDisabled, + toggleFilterNegated, + toggleFilterPinned, + unpinFilter, +} from '../filters'; +import { FilterItem } from './filter_item'; interface Props { filters: MetaFilter[]; - onToggleNegate: (filter: MetaFilter) => void; - onToggleDisabled: (filter: MetaFilter) => void; - onTogglePin: (filter: MetaFilter) => void; - onDelete: (filter: MetaFilter) => void; + onFiltersUpdated: (filters: MetaFilter[]) => void; className: string; } export class FilterBar extends Component { - public onToggleNegate = (filter: MetaFilter) => { - this.props.onToggleNegate(filter); + public onToggleNegated = (i: number) => { + const filters = [...this.props.filters]; + filters[i] = toggleFilterNegated(filters[i]); + this.props.onFiltersUpdated(filters); }; - public onTogglePin = (filter: MetaFilter) => { - this.props.onTogglePin(filter); + public onTogglePinned = (i: number) => { + const filters = [...this.props.filters]; + filters[i] = toggleFilterPinned(filters[i]); + this.props.onFiltersUpdated(filters); }; - public onToggleDisabled = (filter: MetaFilter) => { - this.props.onToggleDisabled(filter); + public onToggleDisabled = (i: number) => { + const filters = [...this.props.filters]; + filters[i] = toggleFilterDisabled(filters[i]); + this.props.onFiltersUpdated(filters); }; - public onDelete = (filter: MetaFilter) => { - this.props.onDelete(filter); + public onAdd = (filter: MetaFilter) => { + const filters = [...this.props.filters, filter]; + this.props.onFiltersUpdated(filters); + }; + + public onRemove = (i: number) => { + const filters = [...this.props.filters]; + filters.splice(i, 1); + this.props.onFiltersUpdated(filters); + }; + + public onUpdate = (i: number, filter: MetaFilter) => { + const filters = [...this.props.filters]; + filters[i] = filter; + this.props.onFiltersUpdated(filters); + }; + + public onEnableAll = () => { + const filters = this.props.filters.map(filter => enableFilter(filter)); + this.props.onFiltersUpdated(filters); + }; + + public onDisableAll = () => { + const filters = this.props.filters.map(filter => disableFilter(filter)); + this.props.onFiltersUpdated(filters); + }; + + public onPinAll = () => { + const filters = this.props.filters.map(filter => pinFilter(filter)); + this.props.onFiltersUpdated(filters); + }; + + public onUnpinAll = () => { + const filters = this.props.filters.map(filter => unpinFilter(filter)); + this.props.onFiltersUpdated(filters); + }; + + public onToggleAllNegated = () => { + const filters = this.props.filters.map(filter => toggleFilterNegated(filter)); + this.props.onFiltersUpdated(filters); + }; + + public onToggleAllDisabled = () => { + const filters = this.props.filters.map(filter => toggleFilterDisabled(filter)); + this.props.onFiltersUpdated(filters); + }; + + public onRemoveAll = () => { + this.props.onFiltersUpdated([]); }; public render() { @@ -57,10 +117,10 @@ export class FilterBar extends Component { this.onTogglePinned(i)} + onToggleNegated={() => this.onToggleNegated(i)} + onToggleDisabled={() => this.onToggleDisabled(i)} + onRemove={() => this.onRemove(i)} /> ); @@ -68,14 +128,35 @@ export class FilterBar extends Component { return ( - {/* TODO display pinned filters first*/} - {filterItems} + + + + + + + {/* TODO display pinned filters first*/} + {filterItems} + + ); } diff --git a/src/ui/public/filter_bar/react/filter_editor/index.tsx b/src/ui/public/filter_bar/react/filter_editor/index.tsx index 277e0a3b517c65..d9c78d245771b0 100644 --- a/src/ui/public/filter_bar/react/filter_editor/index.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/index.tsx @@ -29,8 +29,9 @@ import { EuiSpacer, EuiSwitch, } from '@elastic/eui'; +import { noop } from 'lodash'; import React, { Component } from 'react'; -import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; +import { MetaFilter } from '../../filters'; const fieldOptions = [ { @@ -88,7 +89,6 @@ const valueOptions = [ ]; interface Props { - foo: string; filter: MetaFilter; } @@ -245,7 +245,7 @@ export class FilterEditor extends Component {
- {}} /> +
)} @@ -254,12 +254,12 @@ export class FilterEditor extends Component { - {}}> + Add - {} : this.resetForm}> + {this.props.filter ? 'Cancel' : 'Reset form'} diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index ca742c6d84f6d6..922784bd11d334 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -20,17 +20,17 @@ import { EuiBadge, EuiContextMenu, EuiPopover } from '@elastic/eui'; import classNames from 'classnames'; import React, { Component } from 'react'; -import { getFilterDisplayText } from 'ui/filter_bar/filters/filter_views'; -import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; -import { FilterEditor } from 'ui/filter_bar/react/filter_editor'; +import { isFilterPinned, MetaFilter } from '../filters'; +import { getFilterDisplayText } from '../filters/filter_views'; +import { FilterEditor } from './filter_editor'; interface Props { filter: MetaFilter; className?: string; - onTogglePin: (filter: MetaFilter) => void; - onToggleNegate: (filter: MetaFilter) => void; + onTogglePinned: (filter: MetaFilter) => void; + onToggleNegated: (filter: MetaFilter) => void; onToggleDisabled: (filter: MetaFilter) => void; - onDelete: (filter: MetaFilter) => void; + onRemove: (filter: MetaFilter) => void; } interface State { @@ -43,14 +43,14 @@ export class FilterItem extends Component { }; public render() { - const filter = this.props.filter; - const { negate, disabled, pinned } = filter; + const { filter } = this.props; + const { negate, disabled } = filter.meta; const classes = classNames( 'globalFilterItem', { 'globalFilterItem-isDisabled': disabled, - 'globalFilterItem-isPinned': pinned, + 'globalFilterItem-isPinned': isFilterPinned(filter), 'globalFilterItem-isExcluded': negate, }, this.props.className @@ -61,7 +61,7 @@ export class FilterItem extends Component { id={'foo'} className={classes} title={'foo'} - iconOnClick={() => this.props.onDelete(filter)} + iconOnClick={() => this.props.onRemove(filter)} iconOnClickAriaLabel={`Delete filter`} iconType="cross" // @ts-ignore @@ -83,11 +83,11 @@ export class FilterItem extends Component { id: 0, items: [ { - name: `${pinned ? 'Unpin' : 'Pin across all apps'}`, + name: `${isFilterPinned(filter) ? 'Unpin' : 'Pin across all apps'}`, icon: 'pin', onClick: () => { this.closePopover(); - this.props.onTogglePin(filter); + this.props.onTogglePinned(filter); }, }, { @@ -100,7 +100,7 @@ export class FilterItem extends Component { icon: `${negate ? 'plusInCircle' : 'minusInCircle'}`, onClick: () => { this.closePopover(); - this.props.onToggleNegate(filter); + this.props.onToggleNegated(filter); }, }, { @@ -116,7 +116,7 @@ export class FilterItem extends Component { icon: 'trash', onClick: () => { this.closePopover(); - this.props.onDelete(filter); + this.props.onRemove(filter); }, }, ], @@ -126,7 +126,7 @@ export class FilterItem extends Component { width: 400, content: (
- +
), }, diff --git a/src/ui/public/search_bar/components/filter_options.tsx b/src/ui/public/search_bar/components/filter_options.tsx index 64d773ba886ba6..50df6449d15b86 100644 --- a/src/ui/public/search_bar/components/filter_options.tsx +++ b/src/ui/public/search_bar/components/filter_options.tsx @@ -22,7 +22,13 @@ import { Component } from 'react'; import React from 'react'; interface Props { - onAction: (action: string) => void; + onEnableAll: () => void; + onDisableAll: () => void; + onPinAll: () => void; + onUnpinAll: () => void; + onToggleAllNegated: () => void; + onToggleAllDisabled: () => void; + onRemoveAll: () => void; } interface State { @@ -53,7 +59,7 @@ export class FilterOptions extends Component { icon: 'eye', onClick: () => { this.closePopover(); - this.props.onAction('enable'); + this.props.onEnableAll(); }, }, { @@ -61,7 +67,7 @@ export class FilterOptions extends Component { icon: 'eyeClosed', onClick: () => { this.closePopover(); - this.props.onAction('disable'); + this.props.onDisableAll(); }, }, { @@ -69,7 +75,7 @@ export class FilterOptions extends Component { icon: 'pin', onClick: () => { this.closePopover(); - this.props.onAction('pin'); + this.props.onPinAll(); }, }, { @@ -77,7 +83,7 @@ export class FilterOptions extends Component { icon: 'pin', onClick: () => { this.closePopover(); - this.props.onAction('unpin'); + this.props.onUnpinAll(); }, }, { @@ -85,7 +91,7 @@ export class FilterOptions extends Component { icon: 'invert', onClick: () => { this.closePopover(); - this.props.onAction('toggleNegate'); + this.props.onToggleAllNegated(); }, }, { @@ -93,7 +99,7 @@ export class FilterOptions extends Component { icon: 'eye', onClick: () => { this.closePopover(); - this.props.onAction('toggleDisabled'); + this.props.onToggleAllDisabled(); }, }, { @@ -101,7 +107,7 @@ export class FilterOptions extends Component { icon: 'trash', onClick: () => { this.closePopover(); - this.props.onAction('delete'); + this.props.onRemoveAll(); }, }, ], diff --git a/src/ui/public/search_bar/components/search_bar.tsx b/src/ui/public/search_bar/components/search_bar.tsx index 384c90f698da7c..b5e3797ae2f987 100644 --- a/src/ui/public/search_bar/components/search_bar.tsx +++ b/src/ui/public/search_bar/components/search_bar.tsx @@ -26,7 +26,6 @@ import { MetaFilter } from 'ui/filter_bar/filters/meta_filter'; import { FilterBar } from 'ui/filter_bar/react'; import { IndexPattern } from 'ui/index_patterns'; import { QueryBar } from 'ui/query_bar'; -import { FilterOptions } from 'ui/search_bar/components/filter_options'; import { Storage } from 'ui/storage'; // TODO combine all the filter actions into a single event handler? @@ -41,11 +40,7 @@ interface Props { indexPatterns: IndexPattern[]; store: Storage; filters: MetaFilter[]; - onToggleFilterNegate: (filter: MetaFilter) => void; - onToggleFilterDisabled: (filter: MetaFilter) => void; - onToggleFilterPin: (filter: MetaFilter) => void; - onFilterDelete: (filter: MetaFilter) => void; - onAllFiltersAction: (action: string) => void; + onFiltersUpdated: (filters: MetaFilter[]) => void; } interface State { @@ -141,27 +136,11 @@ export class SearchBar extends Component { this.filterBarRef = node; }} > - - - - - - - - - +

From 5ea40de8e1968986a134175fe2f8cc12feb8cb3d Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 19 Dec 2018 11:19:55 -0700 Subject: [PATCH 26/96] Watch by reference so pin/unpin triggers changes --- src/legacy/core_plugins/kibana/public/discover/index.html | 1 + .../filter_bar/filters/filter_views/phrase_filter_views.tsx | 2 +- src/ui/public/filter_bar/react/filter_editor/index.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index d60227d1a9259c..0f4a1ccc8fbd6d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -35,6 +35,7 @@

index-patterns="[indexPattern]" filters="filters" on-filters-updated="onFiltersUpdated" + watch-depth="reference" >

diff --git a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx index 9e258048faa510..a624329c77e032 100644 --- a/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx +++ b/src/ui/public/filter_bar/filters/filter_views/phrase_filter_views.tsx @@ -23,7 +23,7 @@ import { FilterViews } from './index'; export function getPhraseFilterViews(filter: PhraseFilter): FilterViews { return { getDisplayText() { - return `${filter.meta.key} : ${filter.meta.value}`; + return `${filter.meta.key}: ${filter.meta.value}`; }, }; } diff --git a/src/ui/public/filter_bar/react/filter_editor/index.tsx b/src/ui/public/filter_bar/react/filter_editor/index.tsx index d9c78d245771b0..fa7f7fcb8ac6ef 100644 --- a/src/ui/public/filter_bar/react/filter_editor/index.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/index.tsx @@ -245,7 +245,7 @@ export class FilterEditor extends Component {
- +
)} From bd0f10c934f2b706e0ce7b802b3b42b9af6b6035 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 2 Jan 2019 16:33:02 -0700 Subject: [PATCH 27/96] Add phrase filter editor --- packages/kbn-es-query/src/filters/index.d.ts | 54 +++ packages/kbn-es-query/src/index.d.ts | 1 + src/ui/public/filter_bar/filters/index.ts | 35 +- .../public/filter_bar/filters/range_filter.ts | 14 +- src/ui/public/filter_bar/react/filter_bar.tsx | 144 ++++--- .../react/filter_editor/field_input.tsx | 66 ++++ .../filter_bar/react/filter_editor/index.tsx | 352 ++++++++---------- .../filter_editor/lib/filter_editor_utils.ts | 66 ++++ .../filter_editor/lib/filter_operators.ts | 72 ++++ .../react/filter_editor/operator_input.tsx | 73 ++++ .../filter_editor/phrase_value_input.tsx | 47 +++ .../public/filter_bar/react/filter_item.tsx | 54 ++- .../public/index_patterns/_index_pattern.d.ts | 13 +- src/ui/public/index_patterns/index.d.ts | 7 +- .../search_bar/components/search_bar.tsx | 1 + 15 files changed, 699 insertions(+), 300 deletions(-) create mode 100644 packages/kbn-es-query/src/filters/index.d.ts create mode 100644 src/ui/public/filter_bar/react/filter_editor/field_input.tsx create mode 100644 src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts create mode 100644 src/ui/public/filter_bar/react/filter_editor/lib/filter_operators.ts create mode 100644 src/ui/public/filter_bar/react/filter_editor/operator_input.tsx create mode 100644 src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx diff --git a/packages/kbn-es-query/src/filters/index.d.ts b/packages/kbn-es-query/src/filters/index.d.ts new file mode 100644 index 00000000000000..084ccfa1f87e8d --- /dev/null +++ b/packages/kbn-es-query/src/filters/index.d.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ExistsFilter, + PhraseFilter, + PhrasesFilter, + QueryFilter, + RangeFilter, +} from 'ui/filter_bar/filters'; +import { RangeFilterParams } from 'ui/filter_bar/filters/range_filter'; +import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; + +export function buildExistsFilter( + field: IndexPatternField, + indexPattern: IndexPattern +): ExistsFilter; + +export function buildPhraseFilter( + field: IndexPatternField, + value: string, + indexPattern: IndexPattern +): PhraseFilter; + +export function buildPhrasesFilter( + field: IndexPatternField, + values: string[], + indexPattern: IndexPattern +): PhrasesFilter; + +export function buildQueryFilter(query: string, index: string): QueryFilter; + +export function buildRangeFilter( + field: IndexPatternField, + params: RangeFilterParams, + indexPattern: IndexPattern, + formattedValue?: string +): RangeFilter; diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts index 79e6903b186448..873636a28889fd 100644 --- a/packages/kbn-es-query/src/index.d.ts +++ b/packages/kbn-es-query/src/index.d.ts @@ -18,3 +18,4 @@ */ export * from './kuery'; +export * from './filters'; diff --git a/src/ui/public/filter_bar/filters/index.ts b/src/ui/public/filter_bar/filters/index.ts index cc68b65c5d9e2f..302256d10668f6 100644 --- a/src/ui/public/filter_bar/filters/index.ts +++ b/src/ui/public/filter_bar/filters/index.ts @@ -33,11 +33,30 @@ export { } from './meta_filter'; // The actual filter types -export { CustomFilter } from './custom_filter'; -export { ExistsFilter } from './exists_filter'; -export { GeoBoundingBoxFilter } from './geo_bounding_box_filter'; -export { GeoPolygonFilter } from './geo_polygon_filter'; -export { PhraseFilter } from './phrase_filter'; -export { PhrasesFilter } from './phrases_filter'; -export { QueryFilter } from './query_filter'; -export { RangeFilter } from './range_filter'; +import { CustomFilter } from './custom_filter'; +import { ExistsFilter } from './exists_filter'; +import { GeoBoundingBoxFilter } from './geo_bounding_box_filter'; +import { GeoPolygonFilter } from './geo_polygon_filter'; +import { PhraseFilter } from './phrase_filter'; +import { PhrasesFilter } from './phrases_filter'; +import { QueryFilter } from './query_filter'; +import { RangeFilter } from './range_filter'; +export { + CustomFilter, + ExistsFilter, + GeoBoundingBoxFilter, + GeoPolygonFilter, + PhraseFilter, + PhrasesFilter, + QueryFilter, + RangeFilter, +}; + +// Any filter associated with a field (used in the filter bar/editor) +export type FieldFilter = + | ExistsFilter + | GeoBoundingBoxFilter + | GeoPolygonFilter + | PhraseFilter + | PhrasesFilter + | RangeFilter; diff --git a/src/ui/public/filter_bar/filters/range_filter.ts b/src/ui/public/filter_bar/filters/range_filter.ts index 9951aff8c19a96..e195fc335f72ca 100644 --- a/src/ui/public/filter_bar/filters/range_filter.ts +++ b/src/ui/public/filter_bar/filters/range_filter.ts @@ -19,14 +19,16 @@ import { FilterMeta, MetaFilter } from './meta_filter'; +export interface RangeFilterParams { + gt?: number | string; + gte?: number | string; + lte?: number | string; + lt?: number | string; +} + export type RangeFilterMeta = FilterMeta & { key: string; // The name of the field - params: { - gt?: number | string; - gte?: number | string; - lte?: number | string; - lt?: number | string; - }; + params: RangeFilterParams; }; export type RangeFilter = MetaFilter & { diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index cdb0a69036fd23..d8a36f9b768d0a 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -20,6 +20,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import classNames from 'classnames'; import React, { Component } from 'react'; +import { IndexPattern } from 'ui/index_patterns'; import { FilterOptions } from 'ui/search_bar/components/filter_options'; import { disableFilter, @@ -28,7 +29,6 @@ import { pinFilter, toggleFilterDisabled, toggleFilterNegated, - toggleFilterPinned, unpinFilter, } from '../filters'; import { FilterItem } from './filter_item'; @@ -37,127 +37,109 @@ interface Props { filters: MetaFilter[]; onFiltersUpdated: (filters: MetaFilter[]) => void; className: string; + indexPatterns: IndexPattern[]; } export class FilterBar extends Component { - public onToggleNegated = (i: number) => { - const filters = [...this.props.filters]; - filters[i] = toggleFilterNegated(filters[i]); - this.props.onFiltersUpdated(filters); - }; + public render() { + const classes = classNames('globalFilterBar', this.props.className); - public onTogglePinned = (i: number) => { - const filters = [...this.props.filters]; - filters[i] = toggleFilterPinned(filters[i]); - this.props.onFiltersUpdated(filters); - }; + return ( + + + + - public onToggleDisabled = (i: number) => { - const filters = [...this.props.filters]; - filters[i] = toggleFilterDisabled(filters[i]); - this.props.onFiltersUpdated(filters); - }; + + + {/* TODO display pinned filters first*/} + {this.renderItems()} + + + + ); + } - public onAdd = (filter: MetaFilter) => { - const filters = [...this.props.filters, filter]; - this.props.onFiltersUpdated(filters); - }; + private renderItems() { + return this.props.filters.map((filter, i) => ( + + this.onUpdate(i, newFilter)} + onRemove={() => this.onRemove(i)} + indexPatterns={this.props.indexPatterns} + /> + + )); + } + + // private onAdd = (filter: MetaFilter) => { + // const filters = [...this.props.filters, filter]; + // this.props.onFiltersUpdated(filters); + // }; - public onRemove = (i: number) => { + private onRemove = (i: number) => { const filters = [...this.props.filters]; filters.splice(i, 1); this.props.onFiltersUpdated(filters); }; - public onUpdate = (i: number, filter: MetaFilter) => { + private onUpdate = (i: number, filter: MetaFilter) => { const filters = [...this.props.filters]; filters[i] = filter; this.props.onFiltersUpdated(filters); }; - public onEnableAll = () => { + private onEnableAll = () => { const filters = this.props.filters.map(filter => enableFilter(filter)); this.props.onFiltersUpdated(filters); }; - public onDisableAll = () => { + private onDisableAll = () => { const filters = this.props.filters.map(filter => disableFilter(filter)); this.props.onFiltersUpdated(filters); }; - public onPinAll = () => { + private onPinAll = () => { const filters = this.props.filters.map(filter => pinFilter(filter)); this.props.onFiltersUpdated(filters); }; - public onUnpinAll = () => { + private onUnpinAll = () => { const filters = this.props.filters.map(filter => unpinFilter(filter)); this.props.onFiltersUpdated(filters); }; - public onToggleAllNegated = () => { + private onToggleAllNegated = () => { const filters = this.props.filters.map(filter => toggleFilterNegated(filter)); this.props.onFiltersUpdated(filters); }; - public onToggleAllDisabled = () => { + private onToggleAllDisabled = () => { const filters = this.props.filters.map(filter => toggleFilterDisabled(filter)); this.props.onFiltersUpdated(filters); }; - public onRemoveAll = () => { + private onRemoveAll = () => { this.props.onFiltersUpdated([]); }; - - public render() { - const classes = classNames('globalFilterBar', this.props.className); - - const filterItems = this.props.filters.map((filter, i) => { - return ( - - this.onTogglePinned(i)} - onToggleNegated={() => this.onToggleNegated(i)} - onToggleDisabled={() => this.onToggleDisabled(i)} - onRemove={() => this.onRemove(i)} - /> - - ); - }); - - return ( - - - - - - - - {/* TODO display pinned filters first*/} - {filterItems} - - - - ); - } } diff --git a/src/ui/public/filter_bar/react/filter_editor/field_input.tsx b/src/ui/public/filter_bar/react/filter_editor/field_input.tsx new file mode 100644 index 00000000000000..9cc89eed54f246 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/field_input.tsx @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; +import React, { Component } from 'react'; +import { IndexPatternField } from 'ui/index_patterns'; + +interface Props { + value?: IndexPatternField; + options: IndexPatternField[]; + onChange: (value?: IndexPatternField) => void; +} + +export class FieldInput extends Component { + public render() { + const options = this.getOptions(); + const selectedOptions = this.getSelectedOptions(options); + return ( + + + + ); + } + + private getOptions(): EuiComboBoxOptionProps[] { + return this.props.options.map(field => ({ label: field.name })); + } + + private getSelectedOptions(options: EuiComboBoxOptionProps[]): EuiComboBoxOptionProps[] { + return options.filter(option => { + return typeof this.props.value !== 'undefined' && option.label === this.props.value.name; + }); + } + + private onChange = (selectedOptions: EuiComboBoxOptionProps[]): void => { + if (selectedOptions.length === 0) { + this.props.onChange(undefined); + } + const [selectedOption] = selectedOptions; + const field = this.props.options.find(option => option.name === selectedOption.label); + this.props.onChange(field); + }; +} diff --git a/src/ui/public/filter_bar/react/filter_editor/index.tsx b/src/ui/public/filter_bar/react/filter_editor/index.tsx index fa7f7fcb8ac6ef..8b3a9275deda9a 100644 --- a/src/ui/public/filter_bar/react/filter_editor/index.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/index.tsx @@ -20,8 +20,6 @@ import { EuiButton, EuiButtonEmpty, - EuiComboBox, - EuiComboBoxOptionProps, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -29,209 +27,79 @@ import { EuiSpacer, EuiSwitch, } from '@elastic/eui'; -import { noop } from 'lodash'; +import { + buildExistsFilter, + buildPhraseFilter, + buildPhrasesFilter, + buildQueryFilter, + buildRangeFilter, +} from '@kbn/es-query'; +import { get } from 'lodash'; import React, { Component } from 'react'; -import { MetaFilter } from '../../filters'; - -const fieldOptions = [ - { - label: 'Fields', - isGroupLabelOption: true, - }, - { - label: 'field_1', - }, - { - label: 'field_2', - }, - { - label: 'field_3', - }, - { - label: 'field_4', - }, -]; -const operatorOptions = [ - { - label: 'Operators', - isGroupLabelOption: true, - }, - { - label: 'IS', - }, - { - label: 'IS NOT', - }, - { - label: 'IS ONE OF', - }, - { - label: 'EXISTS', - }, -]; -const valueOptions = [ - { - label: 'Values', - isGroupLabelOption: true, - }, - { - label: 'Value 1', - }, - { - label: 'Value 2', - }, - { - label: 'Value 3', - }, - { - label: 'Value 4', - }, -]; +import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; +import { FieldFilter, MetaFilter } from '../../filters'; +import { FieldInput } from './field_input'; +import { + getFieldFromFilter, + getFilterableFields, + getFilterParams, + getIndexPatternFromFilter, + getOperatorFromFilter, +} from './lib/filter_editor_utils'; +import { Operator } from './lib/filter_operators'; +import { OperatorInput } from './operator_input'; +import { PhraseValueInput } from './phrase_value_input'; interface Props { filter: MetaFilter; + indexPatterns: IndexPattern[]; + onSubmit: (filter: MetaFilter) => void; + onCancel: () => void; } interface State { - selectedField: EuiComboBoxOptionProps[]; - selectedOperand: EuiComboBoxOptionProps[]; - selectedValues: EuiComboBoxOptionProps[]; - valueOptions: EuiComboBoxOptionProps[]; - operatorOptions: EuiComboBoxOptionProps[]; - fieldOptions: EuiComboBoxOptionProps[]; + selectedField?: IndexPatternField; + selectedOperator?: Operator; + params: any; useCustomLabel: boolean; customLabel: string | null; } export class FilterEditor extends Component { - public state = { - fieldOptions, - operatorOptions, - valueOptions, - selectedField: [], - selectedOperand: [], - selectedValues: [], - useCustomLabel: false, - customLabel: null, - }; - - public onFieldChange = (selectedOptions: EuiComboBoxOptionProps[]) => { - // We should only get back either 0 or 1 options. - this.setState({ - selectedField: selectedOptions, - }); - }; - - public onOperandChange = (selectedOptions: EuiComboBoxOptionProps[]) => { - // We should only get back either 0 or 1 options. - this.setState({ - selectedOperand: selectedOptions, - }); - }; - - public onValuesChange = (selectedOptions: EuiComboBoxOptionProps[]) => { - this.setState({ - selectedValues: selectedOptions, - }); - }; - - public onCustomLabelSwitchChange = (event: React.ChangeEvent) => { - this.setState({ - useCustomLabel: event.target.checked, - }); - }; - - public onFieldSearchChange = (searchValue: string) => { - this.setState({ - fieldOptions: fieldOptions.filter(option => - option.label.toLowerCase().includes(searchValue.toLowerCase()) - ), - }); - }; - - public onOperandSearchChange = (searchValue: string) => { - this.setState({ - operatorOptions: operatorOptions.filter(option => - option.label.toLowerCase().includes(searchValue.toLowerCase()) - ), - }); - }; - - public onValuesSearchChange = (searchValue: string) => { - this.setState({ - valueOptions: valueOptions.filter(option => - option.label.toLowerCase().includes(searchValue.toLowerCase()) - ), - }); - }; - - public resetForm = () => { - this.setState({ - selectedField: [], - selectedOperand: [], - selectedValues: [], - useCustomLabel: false, - customLabel: null, - }); - }; + public constructor(props: Props) { + super(props); + this.state = { + selectedField: this.getSelectedField(), + selectedOperator: this.getSelectedOperator(), + params: getFilterParams(props.filter), + useCustomLabel: props.filter.meta.alias !== null, + customLabel: props.filter.meta.alias, + }; + } public render() { return (
- - - + - - - + -
- - - -
+
{this.renderParamsEditor()}
@@ -245,7 +113,10 @@ export class FilterEditor extends Component {
- +
)} @@ -254,25 +125,122 @@ export class FilterEditor extends Component { - - Add + + Save - - {this.props.filter ? 'Cancel' : 'Reset form'} + + Cancel - - {this.props.filter && ( - - Delete - - )} -
); } + + private getFieldOptions() { + return getFilterableFields(this.props.indexPatterns); + } + + private getSelectedIndexPattern() { + return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); + } + + private getSelectedField() { + const indexPattern = this.getSelectedIndexPattern(); + return indexPattern && getFieldFromFilter(this.props.filter as FieldFilter, indexPattern); + } + + private getSelectedOperator() { + return getOperatorFromFilter(this.props.filter); + } + + private onFieldChange = (selectedField?: IndexPatternField) => { + const selectedOperator = undefined; + const params = undefined; + this.setState({ selectedField, selectedOperator, params }); + }; + + private onOperatorChange = (selectedOperator?: Operator) => { + // Only reset params when the operator type changes + const params = + get(this.state.selectedOperator, 'type') === get(selectedOperator, 'type') + ? this.state.params + : undefined; + this.setState({ selectedOperator, params }); + }; + + private onCustomLabelSwitchChange = (event: React.ChangeEvent) => { + this.setState({ + useCustomLabel: event.target.checked, + customLabel: event.target.checked ? '' : null, + }); + }; + + private onCustomLabelChange = (event: React.ChangeEvent) => { + this.setState({ + customLabel: event.target.value, + }); + }; + + private onParamsChange = (params: any) => { + this.setState({ params }); + }; + + private onSubmit = () => { + const filter: MetaFilter | null = this.buildFilter(); + if (filter === null) { + throw new Error('Cannot call onSubmit with empty filter'); + } + filter.meta.negate = get(this.state.selectedOperator, 'negate') || false; + filter.meta.alias = this.state.useCustomLabel ? this.state.customLabel : null; + filter.$state = { + store: this.props.filter.$state.store, + }; + this.props.onSubmit(filter); + }; + + private buildFilter(): MetaFilter | null { + const { selectedField, selectedOperator, params } = this.state; + const indexPattern = this.getSelectedIndexPattern(); + if (!selectedField || !selectedOperator || !indexPattern) { + return null; + } + switch (selectedOperator.type) { + case 'phrase': + return buildPhraseFilter(selectedField, params, indexPattern); + case 'phrases': + return buildPhrasesFilter(selectedField, params.phrases, indexPattern); + case 'range': + const newParams = { gte: params.range.from, lt: params.range.to }; + return buildRangeFilter(selectedField, newParams, indexPattern); + case 'exists': + return buildExistsFilter(selectedField, indexPattern); + case 'query': + return buildQueryFilter(params.query, indexPattern.id); + default: + throw new Error(`Unknown operator type: ${selectedOperator.type}`); + } + } + + private renderParamsEditor() { + const indexPattern = this.getSelectedIndexPattern(); + if (!indexPattern || !this.state.selectedOperator) { + return ''; + } + + switch (this.state.selectedOperator.type) { + case 'phrase': + return ( + + ); + } + } } diff --git a/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts b/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts new file mode 100644 index 00000000000000..a03b485570af89 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { omit } from 'lodash'; +import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; +import { FieldFilter, MetaFilter, PhraseFilter } from '../../../filters'; +import { FILTER_OPERATORS } from './filter_operators'; + +export function getIndexPatternFromFilter( + filter: MetaFilter, + indexPatterns: IndexPattern[] +): IndexPattern | undefined { + return indexPatterns.find(indexPattern => indexPattern.id === filter.meta.index); +} + +export function getFieldFromFilter(filter: FieldFilter, indexPattern: IndexPattern) { + return indexPattern.fields.find((field: any) => field.name === filter.meta.key); +} + +export function getOperatorFromFilter(filter: MetaFilter | undefined) { + return ( + filter && + FILTER_OPERATORS.find(operator => { + return filter.meta.type === operator.type && filter.meta.negate === operator.negate; + }) + ); +} + +export function getQueryDslFromFilter(filter: MetaFilter) { + return omit(filter, ['$state', 'meta']); +} + +export function getFilterableFields(indexPatterns: IndexPattern[]) { + return indexPatterns.reduce((fields: IndexPatternField[], indexPattern) => { + const filterableFields = indexPattern.fields.filter(field => field.filterable); + return [...fields, ...filterableFields]; + }, []); +} + +export function getOperatorOptions(field: IndexPatternField) { + return FILTER_OPERATORS.filter(operator => { + return !operator.fieldTypes || operator.fieldTypes.includes(field.type); + }); +} + +export function getFilterParams(filter?: MetaFilter): any { + if (filter && filter.meta.type === 'phrase') { + return (filter as PhraseFilter).meta.params.query; + } +} diff --git a/src/ui/public/filter_bar/react/filter_editor/lib/filter_operators.ts b/src/ui/public/filter_bar/react/filter_editor/lib/filter_operators.ts new file mode 100644 index 00000000000000..9983f179af306e --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/lib/filter_operators.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export interface Operator { + label: string; + type: string; + negate: boolean; + fieldTypes?: string[]; +} + +export const FILTER_OPERATORS: Operator[] = [ + { + label: 'is', + type: 'phrase', + negate: false, + }, + { + label: 'is not', + type: 'phrase', + negate: true, + }, + { + label: 'is one of', + type: 'phrases', + negate: false, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], + }, + { + label: 'is not one of', + type: 'phrases', + negate: true, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], + }, + { + label: 'is between', + type: 'range', + negate: false, + fieldTypes: ['number', 'date', 'ip'], + }, + { + label: 'is not between', + type: 'range', + negate: true, + fieldTypes: ['number', 'date', 'ip'], + }, + { + label: 'exists', + type: 'exists', + negate: false, + }, + { + label: 'does not exist', + type: 'exists', + negate: true, + }, +]; diff --git a/src/ui/public/filter_bar/react/filter_editor/operator_input.tsx b/src/ui/public/filter_bar/react/filter_editor/operator_input.tsx new file mode 100644 index 00000000000000..941254d1dfae56 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/operator_input.tsx @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; +import React, { Component } from 'react'; +import { IndexPatternField } from 'ui/index_patterns'; +import { getOperatorOptions } from './lib/filter_editor_utils'; +import { FILTER_OPERATORS, Operator } from './lib/filter_operators'; + +interface Props { + field?: IndexPatternField; + value?: Operator; + onChange: (value?: Operator) => void; +} + +export class OperatorInput extends Component { + public render() { + const options = this.getOptions(); + const selectedOptions = this.getSelectedOptions(options); + return ( + + + + ); + } + + private getOptions(): EuiComboBoxOptionProps[] { + if (!this.props.field) { + return []; + } + const options = getOperatorOptions(this.props.field); + return options.map(({ label }) => ({ label })); + } + + private getSelectedOptions(options: EuiComboBoxOptionProps[]): EuiComboBoxOptionProps[] { + return options.filter(option => { + return typeof this.props.value !== 'undefined' && option.label === this.props.value.label; + }); + } + + private onChange = (selectedOptions: EuiComboBoxOptionProps[]): void => { + if (selectedOptions.length === 0) { + this.props.onChange(undefined); + } + const [selectedOption] = selectedOptions; + const operator = FILTER_OPERATORS.find(({ label }) => label === selectedOption.label); + this.props.onChange(operator); + }; +} diff --git a/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx b/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx new file mode 100644 index 00000000000000..d1613e53b33365 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import React, { Component } from 'react'; +import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; + +interface Props { + indexPattern?: IndexPattern; + field?: IndexPatternField; + value?: string; + onChange: (value: string) => void; +} + +export class PhraseValueInput extends Component { + public render() { + return ( + + + + ); + } + + private onChange = (e: React.ChangeEvent) => { + this.props.onChange(e.target.value); + }; +} diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index 922784bd11d334..a84526e0b62ae9 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -20,17 +20,23 @@ import { EuiBadge, EuiContextMenu, EuiPopover } from '@elastic/eui'; import classNames from 'classnames'; import React, { Component } from 'react'; -import { isFilterPinned, MetaFilter } from '../filters'; +import { IndexPattern } from 'ui/index_patterns'; +import { + isFilterPinned, + MetaFilter, + toggleFilterDisabled, + toggleFilterNegated, + toggleFilterPinned, +} from '../filters'; import { getFilterDisplayText } from '../filters/filter_views'; import { FilterEditor } from './filter_editor'; interface Props { filter: MetaFilter; + indexPatterns: IndexPattern[]; className?: string; - onTogglePinned: (filter: MetaFilter) => void; - onToggleNegated: (filter: MetaFilter) => void; - onToggleDisabled: (filter: MetaFilter) => void; - onRemove: (filter: MetaFilter) => void; + onUpdate: (filter: MetaFilter) => void; + onRemove: () => void; } interface State { @@ -61,7 +67,7 @@ export class FilterItem extends Component { id={'foo'} className={classes} title={'foo'} - iconOnClick={() => this.props.onRemove(filter)} + iconOnClick={() => this.props.onRemove()} iconOnClickAriaLabel={`Delete filter`} iconType="cross" // @ts-ignore @@ -87,7 +93,7 @@ export class FilterItem extends Component { icon: 'pin', onClick: () => { this.closePopover(); - this.props.onTogglePinned(filter); + this.onTogglePinned(); }, }, { @@ -100,7 +106,7 @@ export class FilterItem extends Component { icon: `${negate ? 'plusInCircle' : 'minusInCircle'}`, onClick: () => { this.closePopover(); - this.props.onToggleNegated(filter); + this.onToggleNegated(); }, }, { @@ -108,7 +114,7 @@ export class FilterItem extends Component { icon: `${disabled ? 'eye' : 'eyeClosed'}`, onClick: () => { this.closePopover(); - this.props.onToggleDisabled(filter); + this.onToggleDisabled(); }, }, { @@ -116,7 +122,7 @@ export class FilterItem extends Component { icon: 'trash', onClick: () => { this.closePopover(); - this.props.onRemove(filter); + this.props.onRemove(); }, }, ], @@ -124,9 +130,15 @@ export class FilterItem extends Component { { id: 1, width: 400, + title: 'Edit filter', content: (
- +
), }, @@ -157,4 +169,24 @@ export class FilterItem extends Component { isPopoverOpen: !this.state.isPopoverOpen, }); }; + + private onSubmit = (filter: MetaFilter) => { + this.closePopover(); + this.props.onUpdate(filter); + }; + + private onTogglePinned = () => { + const filter = toggleFilterPinned(this.props.filter); + this.props.onUpdate(filter); + }; + + private onToggleNegated = () => { + const filter = toggleFilterNegated(this.props.filter); + this.props.onUpdate(filter); + }; + + private onToggleDisabled = () => { + const filter = toggleFilterDisabled(this.props.filter); + this.props.onUpdate(filter); + }; } diff --git a/src/ui/public/index_patterns/_index_pattern.d.ts b/src/ui/public/index_patterns/_index_pattern.d.ts index 0ff899839c42cc..2738a3949db43c 100644 --- a/src/ui/public/index_patterns/_index_pattern.d.ts +++ b/src/ui/public/index_patterns/_index_pattern.d.ts @@ -21,7 +21,18 @@ * WARNING: these types are incomplete */ -export type IndexPattern = any; +export interface IndexPattern { + id: string; + fields: IndexPatternField[]; + title: string; +} + +export interface IndexPatternField { + name: string; + type: string; + aggregatable: boolean; + filterable: boolean; +} export interface StaticIndexPatternField { name: string; diff --git a/src/ui/public/index_patterns/index.d.ts b/src/ui/public/index_patterns/index.d.ts index 92f04543c237e2..79fe189cdbf99f 100644 --- a/src/ui/public/index_patterns/index.d.ts +++ b/src/ui/public/index_patterns/index.d.ts @@ -17,4 +17,9 @@ * under the License. */ -export { IndexPattern, StaticIndexPattern } from 'ui/index_patterns/_index_pattern'; +export { + IndexPattern, + IndexPatternField, + StaticIndexPattern, + StaticIndexPatternField, +} from 'ui/index_patterns/_index_pattern'; diff --git a/src/ui/public/search_bar/components/search_bar.tsx b/src/ui/public/search_bar/components/search_bar.tsx index b5e3797ae2f987..0e7843ed39cb19 100644 --- a/src/ui/public/search_bar/components/search_bar.tsx +++ b/src/ui/public/search_bar/components/search_bar.tsx @@ -140,6 +140,7 @@ export class SearchBar extends Component { className="globalFilterGroup__filterBar" filters={this.props.filters} onFiltersUpdated={this.props.onFiltersUpdated} + indexPatterns={this.props.indexPatterns} /> From f0e2525cf6ce77d539f26a9b8f99d81da351ae03 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 2 Jan 2019 16:47:24 -0700 Subject: [PATCH 28/96] Update index pattern type def --- src/ui/public/index_patterns/_index_pattern.d.ts | 1 + src/ui/public/timefilter/get_time.ts | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/public/index_patterns/_index_pattern.d.ts b/src/ui/public/index_patterns/_index_pattern.d.ts index 2738a3949db43c..83503ba38c1da6 100644 --- a/src/ui/public/index_patterns/_index_pattern.d.ts +++ b/src/ui/public/index_patterns/_index_pattern.d.ts @@ -25,6 +25,7 @@ export interface IndexPattern { id: string; fields: IndexPatternField[]; title: string; + timeFieldName?: string; } export interface IndexPatternField { diff --git a/src/ui/public/timefilter/get_time.ts b/src/ui/public/timefilter/get_time.ts index 4baff33e7e661f..223d5deca3b84c 100644 --- a/src/ui/public/timefilter/get_time.ts +++ b/src/ui/public/timefilter/get_time.ts @@ -18,8 +18,7 @@ */ import dateMath from '@elastic/datemath'; -import { find } from 'lodash'; -import { IndexPattern } from 'ui/index_patterns'; +import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; interface CalculateBoundsOptions { forceNow?: Date; @@ -58,8 +57,9 @@ export function getTime( } let filter: Filter; - const timefield: { name: string } | undefined = - indexPattern.timeFieldName && find(indexPattern.fields, { name: indexPattern.timeFieldName }); + const timefield: IndexPatternField | undefined = indexPattern.fields.find( + field => field.name === indexPattern.timeFieldName + ); if (!timefield) { return; From 80732b7296e269c79279acc8ef4a3d99f8d6ed47 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 4 Jan 2019 08:35:39 -0700 Subject: [PATCH 29/96] Add filter dialog --- .../public/filter_bar/filters/meta_filter.ts | 18 ++++- src/ui/public/filter_bar/react/filter_bar.tsx | 74 +++++++++++++++++-- .../filter_editor/lib/filter_editor_utils.ts | 15 ++-- 3 files changed, 90 insertions(+), 17 deletions(-) diff --git a/src/ui/public/filter_bar/filters/meta_filter.ts b/src/ui/public/filter_bar/filters/meta_filter.ts index 5477e498bac17c..41f8e2c78e7993 100644 --- a/src/ui/public/filter_bar/filters/meta_filter.ts +++ b/src/ui/public/filter_bar/filters/meta_filter.ts @@ -27,11 +27,12 @@ export interface FilterState { } export interface FilterMeta { - type: string; + // index and type are optional only because when you create a new filter, there are no defaults + index?: string; + type?: string; disabled: boolean; negate: boolean; alias: string | null; - index: string; } export interface MetaFilter { @@ -45,6 +46,19 @@ export interface LatLon { lon: number; } +export function buildEmptyFilter(isPinned: boolean, index?: string): MetaFilter { + const meta: FilterMeta = { + disabled: false, + negate: false, + alias: null, + index, + }; + const $state: FilterState = { + store: isPinned ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE, + }; + return { meta, $state }; +} + export function isFilterPinned(filter: MetaFilter) { return filter.$state.store === FilterStateStore.GLOBAL_STATE; } diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index d8a36f9b768d0a..b354db6c879095 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -17,9 +17,12 @@ * under the License. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; import classNames from 'classnames'; import React, { Component } from 'react'; +import chrome from 'ui/chrome'; +import { buildEmptyFilter } from 'ui/filter_bar/filters/meta_filter'; +import { FilterEditor } from 'ui/filter_bar/react/filter_editor'; import { IndexPattern } from 'ui/index_patterns'; import { FilterOptions } from 'ui/search_bar/components/filter_options'; import { @@ -33,6 +36,8 @@ import { } from '../filters'; import { FilterItem } from './filter_item'; +const config = chrome.getUiSettingsClient(); + interface Props { filters: MetaFilter[]; onFiltersUpdated: (filters: MetaFilter[]) => void; @@ -40,7 +45,15 @@ interface Props { indexPatterns: IndexPattern[]; } -export class FilterBar extends Component { +interface State { + isAddFilterPopoverOpen: boolean; +} + +export class FilterBar extends Component { + public state = { + isAddFilterPopoverOpen: false, + }; + public render() { const classes = classNames('globalFilterBar', this.props.className); @@ -73,6 +86,7 @@ export class FilterBar extends Component { > {/* TODO display pinned filters first*/} {this.renderItems()} + {this.renderAddFilter()} @@ -92,10 +106,46 @@ export class FilterBar extends Component { )); } - // private onAdd = (filter: MetaFilter) => { - // const filters = [...this.props.filters, filter]; - // this.props.onFiltersUpdated(filters); - // }; + private renderAddFilter() { + const isPinned = config.get('filters:pinnedByDefault'); + const [indexPattern] = this.props.indexPatterns; + const index = indexPattern && indexPattern.id; + const newFilter = buildEmptyFilter(isPinned, index); + + const button = ( + + + Add filter + + ); + + return ( + + +
+ +
+
+
+ ); + } + + private onAdd = (filter: MetaFilter) => { + this.onCloseAddFilterPopover(); + const filters = [...this.props.filters, filter]; + this.props.onFiltersUpdated(filters); + }; private onRemove = (i: number) => { const filters = [...this.props.filters]; @@ -142,4 +192,16 @@ export class FilterBar extends Component { private onRemoveAll = () => { this.props.onFiltersUpdated([]); }; + + private onOpenAddFilterPopover = () => { + this.setState({ + isAddFilterPopoverOpen: true, + }); + }; + + private onCloseAddFilterPopover = () => { + this.setState({ + isAddFilterPopoverOpen: false, + }); + }; } diff --git a/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts b/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts index a03b485570af89..f894eeb2e89a20 100644 --- a/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts +++ b/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts @@ -33,13 +33,10 @@ export function getFieldFromFilter(filter: FieldFilter, indexPattern: IndexPatte return indexPattern.fields.find((field: any) => field.name === filter.meta.key); } -export function getOperatorFromFilter(filter: MetaFilter | undefined) { - return ( - filter && - FILTER_OPERATORS.find(operator => { - return filter.meta.type === operator.type && filter.meta.negate === operator.negate; - }) - ); +export function getOperatorFromFilter(filter: MetaFilter) { + return FILTER_OPERATORS.find(operator => { + return filter.meta.type === operator.type && filter.meta.negate === operator.negate; + }); } export function getQueryDslFromFilter(filter: MetaFilter) { @@ -59,8 +56,8 @@ export function getOperatorOptions(field: IndexPatternField) { }); } -export function getFilterParams(filter?: MetaFilter): any { - if (filter && filter.meta.type === 'phrase') { +export function getFilterParams(filter: MetaFilter): any { + if (filter.meta.type === 'phrase') { return (filter as PhraseFilter).meta.params.query; } } From 31a29884cb76deae7f8aca9426a4f95fd789c71e Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 4 Jan 2019 15:07:52 -0700 Subject: [PATCH 30/96] Add phrases editor --- .../filter_bar/react/filter_editor/index.tsx | 14 +++- .../filter_editor/lib/filter_editor_utils.ts | 9 ++- .../filter_editor/phrases_values_input.tsx | 67 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx diff --git a/src/ui/public/filter_bar/react/filter_editor/index.tsx b/src/ui/public/filter_bar/react/filter_editor/index.tsx index 8b3a9275deda9a..d0513e1b771016 100644 --- a/src/ui/public/filter_bar/react/filter_editor/index.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/index.tsx @@ -49,6 +49,7 @@ import { import { Operator } from './lib/filter_operators'; import { OperatorInput } from './operator_input'; import { PhraseValueInput } from './phrase_value_input'; +import { PhrasesValuesInput } from './phrases_values_input'; interface Props { filter: MetaFilter; @@ -212,7 +213,7 @@ export class FilterEditor extends Component { case 'phrase': return buildPhraseFilter(selectedField, params, indexPattern); case 'phrases': - return buildPhrasesFilter(selectedField, params.phrases, indexPattern); + return buildPhrasesFilter(selectedField, params, indexPattern); case 'range': const newParams = { gte: params.range.from, lt: params.range.to }; return buildRangeFilter(selectedField, newParams, indexPattern); @@ -232,6 +233,8 @@ export class FilterEditor extends Component { } switch (this.state.selectedOperator.type) { + case 'exists': + return ''; case 'phrase': return ( { onChange={this.onParamsChange} /> ); + case 'phrases': + return ( + + ); } } } diff --git a/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts b/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts index f894eeb2e89a20..cd3dc3799e8758 100644 --- a/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts +++ b/src/ui/public/filter_bar/react/filter_editor/lib/filter_editor_utils.ts @@ -19,7 +19,7 @@ import { omit } from 'lodash'; import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; -import { FieldFilter, MetaFilter, PhraseFilter } from '../../../filters'; +import { FieldFilter, MetaFilter, PhraseFilter, PhrasesFilter } from '../../../filters'; import { FILTER_OPERATORS } from './filter_operators'; export function getIndexPatternFromFilter( @@ -57,7 +57,10 @@ export function getOperatorOptions(field: IndexPatternField) { } export function getFilterParams(filter: MetaFilter): any { - if (filter.meta.type === 'phrase') { - return (filter as PhraseFilter).meta.params.query; + switch (filter.meta.type) { + case 'phrase': + return (filter as PhraseFilter).meta.params.query; + case 'phrases': + return (filter as PhrasesFilter).meta.params; } } diff --git a/src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx b/src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx new file mode 100644 index 00000000000000..40bdf8ec968d79 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; +import React, { Component } from 'react'; +import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; + +interface Props { + indexPattern?: IndexPattern; + field?: IndexPatternField; + values?: string[]; + onChange: (values: string[]) => void; +} + +export class PhrasesValuesInput extends Component { + public render() { + const options = this.getOptions(); + const selectedOptions = this.getSelectedOptions(options); + return ( + + + + ); + } + + private getOptions(): EuiComboBoxOptionProps[] { + return (this.props.values || []).map(label => ({ label })); + } + + private getSelectedOptions(options: EuiComboBoxOptionProps[]): EuiComboBoxOptionProps[] { + return options.filter(option => { + return (this.props.values || []).includes(option.label); + }); + } + + private onAdd = (value: string) => { + const values = this.props.values || []; + this.props.onChange([...values, value]); + }; + + private onChange = (selectedOptions: EuiComboBoxOptionProps[]) => { + this.props.onChange(selectedOptions.map(option => option.label)); + }; +} From e6f0604ff429781c7fb5a09811e30897018bfc06 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Fri, 4 Jan 2019 19:05:46 -0500 Subject: [PATCH 31/96] Partially implemented the type specific input component. Still need to migrate the validator directives and fix a couple bugs. --- .../filter_editor/phrase_value_input.tsx | 16 ++-- .../react/filter_editor/value_input_type.tsx | 94 +++++++++++++++++++ 2 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx diff --git a/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx b/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx index d1613e53b33365..5975f3d3762a12 100644 --- a/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx @@ -17,31 +17,29 @@ * under the License. */ -import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { EuiFormRow } from '@elastic/eui'; import React, { Component } from 'react'; +import { ValueInputType } from 'ui/filter_bar/react/filter_editor/value_input_type'; import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; interface Props { indexPattern?: IndexPattern; field?: IndexPatternField; value?: string; - onChange: (value: string) => void; + onChange: (value: string | number | boolean) => void; } export class PhraseValueInput extends Component { public render() { return ( - ); } - - private onChange = (e: React.ChangeEvent) => { - this.props.onChange(e.target.value); - }; } diff --git a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx new file mode 100644 index 00000000000000..117aa59fc404a4 --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EuiFieldNumber, EuiFieldText, EuiSelect } from '@elastic/eui'; +import React, { Component } from 'react'; + +interface Props { + value?: string | number; + type: string; + onChange: (value: string | number | boolean) => void; + placeholder: string; +} + +export class ValueInputType extends Component { + public render() { + let inputElement: React.ReactNode; + switch (this.props.type) { + case 'string': + inputElement = ( + + ); + break; + case 'number': + inputElement = ( + + ); + break; + case 'date': + inputElement = ( + + ); + break; + case 'ip': + inputElement = ( + + ); + break; + case 'boolean': + inputElement = ( + + ); + break; + default: + break; + } + + return inputElement; + } + + private onBoolChange = (event: React.ChangeEvent) => { + const boolValue = event.target.value === 'true'; + this.props.onChange(boolValue); + }; + + private onChange = (event: React.ChangeEvent) => { + this.props.onChange(event.target.value); + }; +} From 71a4975cc834d81372657fc5ddd62b3a4d80cef3 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 4 Jan 2019 17:34:51 -0700 Subject: [PATCH 32/96] Add query DSL editor --- src/ui/public/filter_bar/react/filter_bar.tsx | 4 +- .../filter_bar/react/filter_editor/index.tsx | 178 ++++++++++++------ .../public/filter_bar/react/filter_item.tsx | 5 +- 3 files changed, 126 insertions(+), 61 deletions(-) diff --git a/src/ui/public/filter_bar/react/filter_bar.tsx b/src/ui/public/filter_bar/react/filter_bar.tsx index b354db6c879095..76227232eb1fb8 100644 --- a/src/ui/public/filter_bar/react/filter_bar.tsx +++ b/src/ui/public/filter_bar/react/filter_bar.tsx @@ -125,10 +125,10 @@ export class FilterBar extends Component { isOpen={this.state.isAddFilterPopoverOpen} closePopover={this.onCloseAddFilterPopover} anchorPosition="downCenter" - panelPaddingSize="none" + withTitle > -
+
{ @@ -75,32 +81,26 @@ export class FilterEditor extends Component { params: getFilterParams(props.filter), useCustomLabel: props.filter.meta.alias !== null, customLabel: props.filter.meta.alias, + queryDsl: JSON.stringify(getQueryDslFromFilter(props.filter), null, 2), + isCustomEditorOpen: this.isUnknownFilterType(), }; } public render() { return (
- - - - - - - - - - + + + Edit filter + + + Edit as Query DSL + + + + -
{this.renderParamsEditor()}
+ {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} @@ -141,6 +141,86 @@ export class FilterEditor extends Component { ); } + private renderRegularEditor() { + return ( +
+ + + + + + + + + + + +
{this.renderParamsEditor()}
+
+ ); + } + + private renderCustomEditor() { + return ( + + + + ); + } + + private renderParamsEditor() { + const indexPattern = this.getSelectedIndexPattern(); + if (!indexPattern || !this.state.selectedOperator) { + return ''; + } + + switch (this.state.selectedOperator.type) { + case 'exists': + return ''; + case 'phrase': + return ( + + ); + case 'phrases': + return ( + + ); + } + } + + private toggleCustomEditor = () => { + const isCustomEditorOpen = !this.state.isCustomEditorOpen; + this.setState({ isCustomEditorOpen }); + }; + + private isUnknownFilterType() { + const { type } = this.props.filter.meta; + return !!type && !['phrase', 'phrases', 'range', 'exists'].includes(type); + } + private getFieldOptions() { return getFilterableFields(this.props.indexPatterns); } @@ -174,28 +254,37 @@ export class FilterEditor extends Component { }; private onCustomLabelSwitchChange = (event: React.ChangeEvent) => { - this.setState({ - useCustomLabel: event.target.checked, - customLabel: event.target.checked ? '' : null, - }); + const useCustomLabel = event.target.checked; + const customLabel = event.target.checked ? '' : null; + this.setState({ useCustomLabel, customLabel }); }; private onCustomLabelChange = (event: React.ChangeEvent) => { - this.setState({ - customLabel: event.target.value, - }); + const customLabel = event.target.value; + this.setState({ customLabel }); }; private onParamsChange = (params: any) => { this.setState({ params }); }; + private onQueryDslChange = (queryDsl: string) => { + this.setState({ queryDsl }); + }; + private onSubmit = () => { - const filter: MetaFilter | null = this.buildFilter(); + const filter: MetaFilter | null = this.state.isCustomEditorOpen + ? this.buildCustomFilter() + : this.buildFilter(); + if (filter === null) { throw new Error('Cannot call onSubmit with empty filter'); } - filter.meta.negate = get(this.state.selectedOperator, 'negate') || false; + + if (!this.state.isCustomEditorOpen) { + filter.meta.negate = get(this.state.selectedOperator, 'negate') || false; + } + filter.meta.alias = this.state.useCustomLabel ? this.state.customLabel : null; filter.$state = { store: this.props.filter.$state.store, @@ -226,33 +315,10 @@ export class FilterEditor extends Component { } } - private renderParamsEditor() { - const indexPattern = this.getSelectedIndexPattern(); - if (!indexPattern || !this.state.selectedOperator) { - return ''; - } - - switch (this.state.selectedOperator.type) { - case 'exists': - return ''; - case 'phrase': - return ( - - ); - case 'phrases': - return ( - - ); - } + private buildCustomFilter(): MetaFilter { + const { negate, index } = this.props.filter.meta; + const newIndex = index || this.props.indexPatterns[0].id; + const customFilter = JSON.parse(this.state.queryDsl); + return { ...customFilter, meta: { negate, index: newIndex } }; } } diff --git a/src/ui/public/filter_bar/react/filter_item.tsx b/src/ui/public/filter_bar/react/filter_item.tsx index a84526e0b62ae9..f439592c697460 100644 --- a/src/ui/public/filter_bar/react/filter_item.tsx +++ b/src/ui/public/filter_bar/react/filter_item.tsx @@ -130,9 +130,8 @@ export class FilterItem extends Component { { id: 1, width: 400, - title: 'Edit filter', content: ( -
+
{ closePopover={this.closePopover} button={badge} anchorPosition="downCenter" - panelPaddingSize="none" + withTitle={true} > From 190ec9effacce2ea11b63d9332e2d8a2e907bfb9 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 7 Jan 2019 14:36:57 -0700 Subject: [PATCH 33/96] Adds in value suggestions for phrase and phrases filter editors As part of this commit, I pulled out some of the logic that is used in KQL autocomplete suggestions into core because it would have otherwise been duplicated. I figured we would want the same behavior for suggesting values in the filter editor as in the autocomplete. --- .../react/filter_editor/field_input.tsx | 2 +- .../filter_bar/react/filter_editor/index.tsx | 3 +- .../react/filter_editor/operator_input.tsx | 2 +- .../react/filter_editor/phrase_suggestor.tsx | 68 +++++++++++++++++++ .../filter_editor/phrase_value_input.tsx | 68 +++++++++++++++---- .../filter_editor/phrases_values_input.tsx | 14 ++-- src/ui/public/value_suggestions/index.ts | 57 ++++++++++++++++ .../public/autocomplete_providers/value.js | 33 ++------- 8 files changed, 195 insertions(+), 52 deletions(-) create mode 100644 src/ui/public/filter_bar/react/filter_editor/phrase_suggestor.tsx create mode 100644 src/ui/public/value_suggestions/index.ts diff --git a/src/ui/public/filter_bar/react/filter_editor/field_input.tsx b/src/ui/public/filter_bar/react/filter_editor/field_input.tsx index 9cc89eed54f246..f93f3ca497ee99 100644 --- a/src/ui/public/filter_bar/react/filter_editor/field_input.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/field_input.tsx @@ -57,7 +57,7 @@ export class FieldInput extends Component { private onChange = (selectedOptions: EuiComboBoxOptionProps[]): void => { if (selectedOptions.length === 0) { - this.props.onChange(undefined); + return this.props.onChange(undefined); } const [selectedOption] = selectedOptions; const field = this.props.options.find(option => option.name === selectedOption.label); diff --git a/src/ui/public/filter_bar/react/filter_editor/index.tsx b/src/ui/public/filter_bar/react/filter_editor/index.tsx index c65781b87b8a1d..301fae5ae50ea5 100644 --- a/src/ui/public/filter_bar/react/filter_editor/index.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/index.tsx @@ -175,7 +175,8 @@ export class FilterEditor extends Component { value={this.state.queryDsl} onChange={this.onQueryDslChange} mode="json" - width="400" + width="100%" + height="250px" /> ); diff --git a/src/ui/public/filter_bar/react/filter_editor/operator_input.tsx b/src/ui/public/filter_bar/react/filter_editor/operator_input.tsx index 941254d1dfae56..d1c56837864d4d 100644 --- a/src/ui/public/filter_bar/react/filter_editor/operator_input.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/operator_input.tsx @@ -64,7 +64,7 @@ export class OperatorInput extends Component { private onChange = (selectedOptions: EuiComboBoxOptionProps[]): void => { if (selectedOptions.length === 0) { - this.props.onChange(undefined); + return this.props.onChange(undefined); } const [selectedOption] = selectedOptions; const operator = FILTER_OPERATORS.find(({ label }) => label === selectedOption.label); diff --git a/src/ui/public/filter_bar/react/filter_editor/phrase_suggestor.tsx b/src/ui/public/filter_bar/react/filter_editor/phrase_suggestor.tsx new file mode 100644 index 00000000000000..cea3afa2beadba --- /dev/null +++ b/src/ui/public/filter_bar/react/filter_editor/phrase_suggestor.tsx @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Component } from 'react'; +import chrome from 'ui/chrome'; +import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; +import { getSuggestions } from 'ui/value_suggestions'; +const config = chrome.getUiSettingsClient(); + +export interface PhraseSuggestorProps { + indexPattern: IndexPattern; + field?: IndexPatternField; +} + +export interface PhraseSuggestorState { + suggestions: string[]; + isLoading: boolean; +} + +export abstract class PhraseSuggestor extends Component< + T, + PhraseSuggestorState +> { + public state: PhraseSuggestorState = { + suggestions: [], + isLoading: false, + }; + + public componentDidMount() { + this.updateSuggestions(); + } + + protected isSuggestingValues() { + const shouldSuggestValues = config.get('filterEditor:suggestValues'); + const { field } = this.props; + return shouldSuggestValues && field && field.aggregatable && field.type === 'string'; + } + + protected onSearchChange = (value: string | number | boolean) => { + this.updateSuggestions(`${value}`); + }; + + protected async updateSuggestions(value: string = '') { + const { indexPattern, field } = this.props as PhraseSuggestorProps; + if (!field || !this.isSuggestingValues()) { + return; + } + this.setState({ isLoading: true }); + const suggestions = await getSuggestions(indexPattern.title, field, value); + this.setState({ suggestions, isLoading: false }); + } +} diff --git a/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx b/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx index 5975f3d3762a12..413aa42dac4201 100644 --- a/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/phrase_value_input.tsx @@ -17,29 +17,71 @@ * under the License. */ -import { EuiFormRow } from '@elastic/eui'; -import React, { Component } from 'react'; +import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; +import { uniq } from 'lodash'; +import React from 'react'; import { ValueInputType } from 'ui/filter_bar/react/filter_editor/value_input_type'; -import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; +import { PhraseSuggestor, PhraseSuggestorProps } from './phrase_suggestor'; -interface Props { - indexPattern?: IndexPattern; - field?: IndexPatternField; +interface Props extends PhraseSuggestorProps { value?: string; onChange: (value: string | number | boolean) => void; } -export class PhraseValueInput extends Component { +export class PhraseValueInput extends PhraseSuggestor { public render() { return ( - + {this.isSuggestingValues() ? ( + this.renderWithSuggestions() + ) : ( + + )} ); } + + private renderWithSuggestions() { + const options = this.getOptions(); + const selectedOptions = this.getSelectedOptions(options); + return ( + + ); + } + + private onComboBoxChange = (selectedOptions: EuiComboBoxOptionProps[]): void => { + if (selectedOptions.length === 0) { + return this.props.onChange(''); + } + const [selectedOption] = selectedOptions; + this.props.onChange(selectedOption.label); + }; + + private getOptions() { + const options = [...this.state.suggestions]; + if (typeof this.props.value !== 'undefined') { + options.unshift(this.props.value); + } + return uniq(options).map(label => ({ label })); + } + + private getSelectedOptions(options: EuiComboBoxOptionProps[]): EuiComboBoxOptionProps[] { + return options.filter(option => { + return typeof this.props.value !== 'undefined' && option.label === this.props.value; + }); + } } diff --git a/src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx b/src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx index 40bdf8ec968d79..b63ad557e6fa59 100644 --- a/src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/phrases_values_input.tsx @@ -18,17 +18,16 @@ */ import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow } from '@elastic/eui'; -import React, { Component } from 'react'; -import { IndexPattern, IndexPatternField } from 'ui/index_patterns'; +import { uniq } from 'lodash'; +import React from 'react'; +import { PhraseSuggestor, PhraseSuggestorProps } from './phrase_suggestor'; -interface Props { - indexPattern?: IndexPattern; - field?: IndexPatternField; +interface Props extends PhraseSuggestorProps { values?: string[]; onChange: (values: string[]) => void; } -export class PhrasesValuesInput extends Component { +export class PhrasesValuesInput extends PhraseSuggestor { public render() { const options = this.getOptions(); const selectedOptions = this.getSelectedOptions(options); @@ -47,7 +46,8 @@ export class PhrasesValuesInput extends Component { } private getOptions(): EuiComboBoxOptionProps[] { - return (this.props.values || []).map(label => ({ label })); + const options = [...(this.props.values || []), ...this.state.suggestions]; + return uniq(options).map(label => ({ label })); } private getSelectedOptions(options: EuiComboBoxOptionProps[]): EuiComboBoxOptionProps[] { diff --git a/src/ui/public/value_suggestions/index.ts b/src/ui/public/value_suggestions/index.ts new file mode 100644 index 00000000000000..d75ef90669d851 --- /dev/null +++ b/src/ui/public/value_suggestions/index.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { memoize } from 'lodash'; +import chrome from 'ui/chrome'; +import { IndexPatternField } from 'ui/index_patterns'; +import { kfetch } from 'ui/kfetch'; + +const config = chrome.getUiSettingsClient(); + +const requestSuggestions = memoize( + (index: string, field: IndexPatternField, query: string, boolFilter: any = []) => { + return kfetch({ + pathname: `/api/kibana/suggestions/values/${index}`, + method: 'POST', + body: JSON.stringify({ query, field: field.name, boolFilter }), + }); + }, + resolver +); + +export async function getSuggestions( + index: string, + field: IndexPatternField, + query: string, + boolFilter?: any +) { + const shouldSuggestValues = config.get('filterEditor:suggestValues'); + if (field.type === 'boolean') { + return [true, false]; + } else if (!shouldSuggestValues || !field.aggregatable || field.type !== 'string') { + return []; + } + return await requestSuggestions(index, field, query, boolFilter); +} + +function resolver(index: string, field: IndexPatternField, query: string, boolFilter: any) { + // Only cache results for a minute + const ttl = Math.floor(Date.now() / 1000 / 60); + return [ttl, query, index, field.name, JSON.stringify(boolFilter)].join('|'); +} diff --git a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js index faea99c8f0f6a4..3af4d5dda98be9 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js @@ -4,21 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten, memoize } from 'lodash'; +import { flatten } from 'lodash'; import { escapeQuotes } from './escape_kuery'; -import { kfetch } from 'ui/kfetch'; +import { getSuggestions } from 'ui/value_suggestions'; const type = 'value'; -const requestSuggestions = memoize((query, field, boolFilter) => { - return kfetch({ - pathname: `/api/kibana/suggestions/values/${field.indexPatternTitle}`, - method: 'POST', - body: JSON.stringify({ query, field: field.name, boolFilter }), - }); -}, resolver); - -export function getSuggestionsProvider({ config, indexPatterns, boolFilter }) { +export function getSuggestionsProvider({ indexPatterns, boolFilter }) { const allFields = flatten( indexPatterns.map(indexPattern => { return indexPattern.fields.map(field => ({ @@ -27,7 +19,6 @@ export function getSuggestionsProvider({ config, indexPatterns, boolFilter }) { })); }) ); - const shouldSuggestValues = config.get('filterEditor:suggestValues'); return function getValueSuggestions({ start, @@ -40,17 +31,7 @@ export function getSuggestionsProvider({ config, indexPatterns, boolFilter }) { const query = `${prefix}${suffix}`; const suggestionsByField = fields.map(field => { - if (field.type === 'boolean') { - return wrapAsSuggestions(start, end, query, ['true', 'false']); - } else if ( - !shouldSuggestValues || - !field.aggregatable || - field.type !== 'string' - ) { - return []; - } - - return requestSuggestions(query, field, boolFilter).then(data => { + return getSuggestions(field.indexPatternTitle, field, query, boolFilter).then(data => { const quotedValues = data.map(value => `"${escapeQuotes(value)}"`); return wrapAsSuggestions(start, end, query, quotedValues); }); @@ -70,9 +51,3 @@ function wrapAsSuggestions(start, end, query, values) { return { type, text, start, end }; }); } - -function resolver(query, field, boolFilter) { - // Only cache results for a minute - const ttl = Math.floor(Date.now() / 1000 / 60); - return [ttl, query, field.indexPatternTitle, field.name, JSON.stringify(boolFilter)].join('|'); -} From 9def223732861975f52ceb35e7978f1134cfe755 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Mon, 7 Jan 2019 15:51:15 -0700 Subject: [PATCH 34/96] Add new filter bar to visualize/dashboard As part of this commit I had to add a couple of extra parameters to the new search bar component to handle whether or not the query bar and filter bar should be shown --- .../public/dashboard/dashboard_app.html | 10 ++- .../kibana/public/dashboard/dashboard_app.js | 23 ++++--- .../public/visualize/editor/editor.html | 14 +++-- .../kibana/public/visualize/editor/editor.js | 10 ++- src/ui/public/search_bar/components/index.tsx | 2 +- .../search_bar/components/search_bar.tsx | 63 ++++++++++++------- 6 files changed, 80 insertions(+), 42 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html index 0aa3c6eb930ccf..b52ec007d0cad8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -30,12 +30,16 @@
- + filters="model.filters" + on-filters-updated="onFiltersUpdated" + show-filter-bar="showFilterBar()" + watch-depth="reference" + >
diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js index f71edb1c5e77bb..b77373183586d1 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -25,7 +25,7 @@ import chrome from 'ui/chrome'; import { applyTheme } from 'ui/theme'; import { toastNotifications } from 'ui/notify'; -import 'ui/query_bar'; +import 'ui/search_bar'; import { panelActionsStore } from './store/panel_actions_store'; @@ -91,7 +91,7 @@ app.directive('dashboardApp', function ($injector) { i18n, ) { const filterManager = Private(FilterManagerProvider); - const filterBar = Private(FilterBarQueryFilterProvider); + const queryFilter = Private(FilterBarQueryFilterProvider); const docTitle = Private(DocTitleProvider); const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider); const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider); @@ -131,6 +131,7 @@ app.directive('dashboardApp', function ($injector) { // https://github.com/angular/angular.js/wiki/Understanding-Scopes $scope.model = { query: dashboardStateManager.getQuery(), + filters: queryFilter.getFilters(), timeRestore: dashboardStateManager.getTimeRestore(), title: dashboardStateManager.getTitle(), description: dashboardStateManager.getDescription(), @@ -154,7 +155,7 @@ app.directive('dashboardApp', function ($injector) { query: '', language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage') }, - filterBar.getFilters() + queryFilter.getFilters() ); timefilter.enableAutoRefreshSelector(); @@ -225,11 +226,16 @@ app.directive('dashboardApp', function ($injector) { dashboardStateManager.requestReload(); } else { $scope.model.query = migrateLegacyQuery(query); - dashboardStateManager.applyFilters($scope.model.query, filterBar.getFilters()); + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); } $scope.refresh(); }; + $scope.onFiltersUpdated = filters => { + // The filters will automatically be set when the queryFilter emits an update event (see below) + queryFilter.setFilters(filters); + }; + updateTheme(); $scope.indexPatterns = []; @@ -350,7 +356,7 @@ app.directive('dashboardApp', function ($injector) { }); } - $scope.showFilterBar = () => filterBar.getFilters().length > 0 || !dashboardStateManager.getFullScreenMode(); + $scope.showFilterBar = () => $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); $scope.showAddPanel = () => { dashboardStateManager.setFullScreenMode(false); @@ -466,12 +472,13 @@ app.directive('dashboardApp', function ($injector) { updateViewMode(dashboardStateManager.getViewMode()); // update root source when filters update - $scope.$listen(filterBar, 'update', function () { - dashboardStateManager.applyFilters($scope.model.query, filterBar.getFilters()); + $scope.$listen(queryFilter, 'update', function () { + $scope.model.filters = queryFilter.getFilters(); + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); }); // update data when filters fire fetch event - $scope.$listen(filterBar, 'fetch', $scope.refresh); + $scope.$listen(queryFilter, 'fetch', $scope.refresh); $scope.$on('$destroy', () => { dashboardStateManager.destroy(); diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html index 5a31a97c569667..bfe058348e91d1 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.html @@ -36,14 +36,18 @@
-
- + + filters="filters" + on-filters-updated="onFiltersUpdated" + show-query-bar="vis.type.requiresSearch && vis.type.options.showQueryBar" + show-filter-bar="vis.type.requiresSearch && vis.type.options.showFilterBar" + watch-depth="reference" + >
diff --git a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js index b554f761f002d4..4f47e6b3d030f8 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/editor/editor.js @@ -23,7 +23,7 @@ import './visualization_editor'; import 'ui/vis/editors/default/sidebar'; import 'ui/visualize'; import 'ui/collapsible_sidebar'; -import 'ui/query_bar'; +import 'ui/search_bar'; import chrome from 'ui/chrome'; import React from 'react'; import angular from 'angular'; @@ -279,6 +279,13 @@ function VisEditor( return appState; }()); + $scope.filters = queryFilter.getFilters(); + + $scope.onFiltersUpdated = filters => { + // The filters will automatically be set when the queryFilter emits an update event (see below) + queryFilter.setFilters(filters); + }; + function init() { // export some objects $scope.savedVis = savedVis; @@ -345,6 +352,7 @@ function VisEditor( // update the searchSource when filters update $scope.$listen(queryFilter, 'update', function () { + $scope.filters = queryFilter.getFilters(); $scope.fetch(); }); diff --git a/src/ui/public/search_bar/components/index.tsx b/src/ui/public/search_bar/components/index.tsx index 39410484e9c7fa..131c6059934a47 100644 --- a/src/ui/public/search_bar/components/index.tsx +++ b/src/ui/public/search_bar/components/index.tsx @@ -17,4 +17,4 @@ * under the License. */ -export { SearchBar } from 'ui/search_bar/components/search_bar'; +export { SearchBar } from './search_bar'; diff --git a/src/ui/public/search_bar/components/search_bar.tsx b/src/ui/public/search_bar/components/search_bar.tsx index 0e7843ed39cb19..8c8383db209320 100644 --- a/src/ui/public/search_bar/components/search_bar.tsx +++ b/src/ui/public/search_bar/components/search_bar.tsx @@ -41,6 +41,8 @@ interface Props { store: Storage; filters: MetaFilter[]; onFiltersUpdated: (filters: MetaFilter[]) => void; + showQueryBar: boolean; + showFilterBar: boolean; } interface State { @@ -48,6 +50,11 @@ interface State { } export class SearchBar extends Component { + public static defaultProps = { + showQueryBar: true, + showFilterBar: true, + }; + public filterBarRef: Element | null = null; public filterBarWrapperRef: Element | null = null; @@ -115,35 +122,43 @@ export class SearchBar extends Component { return (
- - -
{ - this.filterBarWrapperRef = node; - }} - className={classes} - > + {this.props.showQueryBar ? ( + + ) : ( + '' + )} + + {this.props.showFilterBar ? (
{ - this.filterBarRef = node; + this.filterBarWrapperRef = node; }} + className={classes} > - +
{ + this.filterBarRef = node; + }} + > + +
-
+ ) : ( + '' + )}
); } From 4991692eccb6636bae703b047efc782906758c2c Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 8 Jan 2019 17:10:57 -0500 Subject: [PATCH 35/96] Account for fields that are mapped as numbers but have string values in _source (ES allows this to happen) --- .../react/filter_editor/value_input_type.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx index 117aa59fc404a4..a25fa872c5f4bb 100644 --- a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx @@ -29,13 +29,14 @@ interface Props { export class ValueInputType extends Component { public render() { + const value = this.props.value; let inputElement: React.ReactNode; switch (this.props.type) { case 'string': inputElement = ( ); @@ -44,7 +45,7 @@ export class ValueInputType extends Component { inputElement = ( ); @@ -53,7 +54,7 @@ export class ValueInputType extends Component { inputElement = ( ); @@ -62,7 +63,7 @@ export class ValueInputType extends Component { inputElement = ( ); @@ -71,7 +72,7 @@ export class ValueInputType extends Component { inputElement = ( ); From ecd0b85510fc71305728182d48967f4cf3d5802c Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 8 Jan 2019 17:40:08 -0500 Subject: [PATCH 36/96] validate date math expressions --- .../public/filter_bar/react/filter_editor/value_input_type.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx index a25fa872c5f4bb..04b73de69b0628 100644 --- a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx @@ -17,6 +17,7 @@ * under the License. */ +import dateMath from '@elastic/datemath'; import { EuiFieldNumber, EuiFieldText, EuiSelect } from '@elastic/eui'; import React, { Component } from 'react'; @@ -51,11 +52,13 @@ export class ValueInputType extends Component { ); break; case 'date': + const moment = typeof value === 'string' ? dateMath.parse(value) : null; inputElement = ( ); break; From 25910b55faddf8d6bd5908030e825204ae3b4b78 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Tue, 8 Jan 2019 19:06:14 -0500 Subject: [PATCH 37/96] validate IP inputs --- .../react/filter_editor/value_input_type.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx index 04b73de69b0628..fc877c61ff0c5d 100644 --- a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx @@ -20,6 +20,7 @@ import dateMath from '@elastic/datemath'; import { EuiFieldNumber, EuiFieldText, EuiSelect } from '@elastic/eui'; import React, { Component } from 'react'; +import Ipv4Address from 'ui/utils/ipv4_address'; interface Props { value?: string | number; @@ -68,6 +69,7 @@ export class ValueInputType extends Component { placeholder={this.props.placeholder} value={value} onChange={this.onChange} + isInvalid={typeof value === 'string' && !this.validateIp(value)} /> ); break; @@ -95,4 +97,18 @@ export class ValueInputType extends Component { private onChange = (event: React.ChangeEvent) => { this.props.onChange(event.target.value); }; + + private validateIp = (ipAddress: string) => { + if (ipAddress == null || ipAddress === '') { + return true; + } + + try { + // @ts-ignore + const ipOjbect = new Ipv4Address(ipAddress); + return true; + } catch (e) { + return false; + } + }; } From 63e2e6d4385b39865a655bdcd92e3cdd9b705d31 Mon Sep 17 00:00:00 2001 From: Matthew Bargar Date: Wed, 9 Jan 2019 12:13:46 -0500 Subject: [PATCH 38/96] make user pick the value they want. This is more in line with how the other types work and it ensures the onChange callback gets fired so that the parent gets the value param for the filter it needs to create. --- .../filter_bar/react/filter_editor/value_input_type.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx index fc877c61ff0c5d..8ae331dcd17ce6 100644 --- a/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx +++ b/src/ui/public/filter_bar/react/filter_editor/value_input_type.tsx @@ -76,7 +76,11 @@ export class ValueInputType extends Component { case 'boolean': inputElement = ( From 92a94024bb9d97a759ee8a2ee6a8dd9f8d1d0d81 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 9 Jan 2019 17:00:36 -0700 Subject: [PATCH 39/96] Add apply filter popover --- .../public/discover/controllers/discover.js | 16 +++ .../kibana/public/discover/index.html | 12 +- .../public/visualize/editor/editor.html | 12 +- .../kibana/public/visualize/editor/editor.js | 16 +++ .../apply_filters/apply_filters_popover.tsx | 114 ++++++++++++++++++ src/ui/public/apply_filters/directive.html | 7 ++ src/ui/public/apply_filters/directive.js | 59 +++++++++ src/ui/public/apply_filters/index.ts | 22 ++++ .../__tests__/filter_bar_click_handler.js | 80 ------------ .../filter_bar/filter_bar_click_handler.js | 89 -------------- .../geo_bounding_box_filter_views.tsx | 5 +- .../filter_views/geo_polygon_filter_views.tsx | 5 +- .../filter_views/range_filter_views.tsx | 23 +--- .../filters/geo_bounding_box_filter.ts | 1 + .../filter_bar/filters/geo_polygon_filter.ts | 1 + .../public/filter_bar/filters/range_filter.ts | 1 + src/ui/public/filter_bar/query_filter.js | 23 ++-- .../react/filter_editor/phrase_suggestor.tsx | 2 +- .../search_bar/components/search_bar.tsx | 1 - .../public/autocomplete_providers/value.js | 2 +- 20 files changed, 269 insertions(+), 222 deletions(-) create mode 100644 src/ui/public/apply_filters/apply_filters_popover.tsx create mode 100644 src/ui/public/apply_filters/directive.html create mode 100644 src/ui/public/apply_filters/directive.js create mode 100644 src/ui/public/apply_filters/index.ts delete mode 100644 src/ui/public/filter_bar/__tests__/filter_bar_click_handler.js delete mode 100644 src/ui/public/filter_bar/filter_bar_click_handler.js diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 55a3ae7fb5c210..858e1f572a125e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -34,6 +34,7 @@ import 'ui/index_patterns'; import 'ui/state_management/app_state'; import { timefilter } from 'ui/timefilter'; import 'ui/search_bar'; +import 'ui/apply_filters'; import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; import { toastNotifications } from 'ui/notify'; import { VisProvider } from 'ui/vis'; @@ -357,6 +358,21 @@ function discoverController( queryFilter.setFilters(filters); }; + $scope.onCancelApplyFilters = () => { + $scope.state.$newFilters = []; + }; + + $scope.onApplyFilters = filters => { + queryFilter.addFiltersAndChangeTimeFilter(filters); + $scope.state.$newFilters = []; + }; + + $scope.$watch('state.$newFilters', (filters = []) => { + if (filters.length === 1) { + $scope.onApplyFilters(filters); + } + }); + const getFieldCounts = async () => { // the field counts aren't set until we have the data back, // so we wait for the fetch to be done before proceeding diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index 0f4a1ccc8fbd6d..7015ae8d2b2a60 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -41,13 +41,13 @@

+ +
-
- -