From 45b24afe831c8db941a04e29bd4d82ac400da7e3 Mon Sep 17 00:00:00 2001 From: Matt Bargar Date: Wed, 30 Jan 2019 17:00:42 -0500 Subject: [PATCH] Revert "Migrate filter bar to React, EUI, and Typescript (#25563)" (#29662) This reverts commit 410c094547e235d5805a94f066117a05381ab5a4. --- packages/kbn-es-query/README.md | 94 ---- packages/kbn-es-query/package.json | 5 +- packages/kbn-es-query/src/filters/exists.js | 1 - packages/kbn-es-query/src/filters/index.d.ts | 47 -- packages/kbn-es-query/src/filters/index.js | 1 - .../src/filters/lib/custom_filter.ts | 24 - .../kbn-es-query/src/filters/lib/index.ts | 50 -- .../src/filters/lib/meta_filter.ts | 100 ---- .../src/filters/lib/phrases_filter.ts | 28 -- .../src/filters/lib/query_string_filter.ts | 26 - packages/kbn-es-query/src/filters/phrase.js | 1 - packages/kbn-es-query/src/filters/phrases.js | 2 - packages/kbn-es-query/src/filters/query.js | 1 - packages/kbn-es-query/src/filters/range.js | 2 - packages/kbn-es-query/src/index.d.ts | 1 - packages/kbn-es-query/tsconfig.json | 8 +- .../fixtures/filters/exists_filter.js | 26 +- .../fixtures/filters/index.js | 16 +- .../filters/phrase_filter.js} | 22 +- .../filters/phrases_filter.js} | 33 +- .../filters/range_filter.js} | 29 +- .../filters/scripted_phrase_filter.js} | 37 +- .../kibana/public/context/app.html | 7 +- .../core_plugins/kibana/public/context/app.js | 1 - .../context/query_parameters/actions.js | 7 - .../public/dashboard/dashboard_app.html | 21 +- .../kibana/public/dashboard/dashboard_app.js | 53 +- .../public/discover/controllers/discover.js | 21 +- .../kibana/public/discover/index.html | 25 +- .../public/visualize/editor/editor.html | 32 +- .../kibana/public/visualize/editor/editor.js | 26 +- src/ui/public/_index.scss | 1 - .../agg_types/filter/agg_type_filters.test.ts | 2 +- .../apply_filters/apply_filters_popover.tsx | 128 ----- src/ui/public/apply_filters/directive.html | 7 - src/ui/public/apply_filters/directive.js | 59 --- .../public/doc_table/components/table_row.js | 2 +- .../public/filter_bar/__tests__/filter_bar.js | 228 +++++++++ .../__tests__/filter_bar_click_handler.js | 80 +++ .../filter_bar/_global_filter_group.scss | 22 - .../filter_bar/_global_filter_item.scss | 37 -- src/ui/public/filter_bar/_index.scss | 2 - src/ui/public/filter_bar/filter_bar.html | 149 ++++++ src/ui/public/filter_bar/filter_bar.js | 213 ++++++++ src/ui/public/filter_bar/filter_bar.less | 226 +++++++++ src/ui/public/filter_bar/filter_bar.tsx | 217 -------- .../filter_bar/filter_bar_click_handler.js | 89 ++++ .../filter_editor/generic_combo_box.tsx | 61 --- .../public/filter_bar/filter_editor/index.tsx | 471 ------------------ .../lib/filter_editor_utils.test.ts | 314 ------------ .../filter_editor/lib/filter_editor_utils.ts | 178 ------- .../filter_editor/lib/filter_operators.ts | 106 ---- .../filter_editor/phrase_suggestor.tsx | 73 --- .../filter_editor/phrase_value_input.tsx | 88 ---- .../filter_editor/phrases_values_input.tsx | 67 --- .../filter_editor/range_value_input.tsx | 121 ----- .../filter_editor/value_input_type.tsx | 120 ----- src/ui/public/filter_bar/filter_item.tsx | 226 --------- .../filter_bar/filter_pill/filter_pill.html | 97 ++++ .../filter_bar/filter_pill/filter_pill.js | 57 +++ .../filter_pill/index.js} | 2 +- .../public/filter_bar/filter_view/index.tsx | 103 ---- .../index.tsx => filter_bar/index.js} | 4 +- src/ui/public/filter_bar/index.ts | 22 - .../lib/__tests__/disable_filter.js | 140 ++++++ .../__tests__/filter_applied_and_unwrap.js | 22 +- .../__tests__/filter_out_time_based_filter.js | 57 +++ .../public/filter_bar/lib/disable_filter.js | 44 ++ .../lib/filter_applied_and_unwrap.js} | 7 +- .../lib/filter_out_time_based_filter.js | 26 +- src/ui/public/filter_bar/lib/map_phrase.js | 8 +- src/ui/public/filter_bar/query_filter.js | 29 +- .../public/filter_editor/filter_editor.html | 148 ++++++ src/ui/public/filter_editor/filter_editor.js | 161 ++++++ .../public/filter_editor/filter_editor.less | 36 ++ .../filter_editor/filter_field_select.html | 27 + .../filter_editor/filter_field_select.js | 52 ++ .../filter_editor/filter_operator_select.html | 12 + .../filter_operator_select.js} | 27 +- .../filter_query_dsl_editor.html | 13 + .../filter_editor/filter_query_dsl_editor.js | 59 +++ .../index.d.ts => filter_editor/index.js} | 2 +- .../lib/__tests__/filter_editor_utils.js | 396 +++++++++++++++ .../filter_editor/lib/filter_editor_utils.js | 111 +++++ .../filter_editor/lib/filter_operators.js | 72 +++ .../params_editor/filter_params_editor.html | 19 + .../params_editor/filter_params_editor.js | 25 +- .../filter_params_input_type.html | 50 ++ .../params_editor/filter_params_input_type.js | 49 ++ .../filter_params_phrase_controller.js | 57 +++ .../filter_params_phrase_editor.html | 45 ++ .../filter_params_phrase_editor.js | 42 ++ .../filter_params_phrases_editor.html | 28 ++ .../filter_params_phrases_editor.js | 39 ++ .../filter_params_range_editor.html | 30 ++ .../filter_params_range_editor.js} | 26 +- src/ui/public/index_patterns/_field.d.ts | 26 - .../public/index_patterns/_index_pattern.d.ts | 9 +- .../public/index_patterns/fixtures/index.ts | 79 --- src/ui/public/index_patterns/index.d.ts | 7 +- .../static_utils/__tests__/index.js | 54 +- .../index_patterns/static_utils/index.d.ts | 11 +- .../index_patterns/static_utils/index.js | 10 +- .../__snapshots__/query_bar.test.tsx.snap | 3 + .../query_bar/components/query_bar.test.tsx | 20 +- .../public/query_bar/components/query_bar.tsx | 7 +- .../components/typeahead/_suggestion.scss | 5 +- .../search_bar/components/filter_options.tsx | 184 ------- .../search_bar/components/search_bar.tsx | 178 ------- src/ui/public/styles/bootstrap_dark.less | 1 + src/ui/public/timefilter/get_time.ts | 8 +- src/ui/public/value_suggestions/index.ts | 24 - .../value_suggestions.test.ts | 131 ----- .../value_suggestions/value_suggestions.ts | 53 -- .../config/editor_config_providers.test.ts | 17 +- .../apps/dashboard/_dashboard_filter_bar.js | 22 +- .../apps/dashboard/_dashboard_filtering.js | 2 +- test/functional/apps/discover/_discover.js | 2 +- .../apps/management/_scripted_fields.js | 9 +- .../functional/page_objects/dashboard_page.js | 4 + test/functional/page_objects/discover_page.js | 7 + .../services/dashboard/expectations.js | 10 +- test/functional/services/filter_bar.js | 86 ++-- .../beats_management/types/kibana.d.ts | 16 + .../autocomplete_providers/__tests__/value.js | 149 +++--- .../public/autocomplete_providers/field.js | 7 +- .../public/autocomplete_providers/operator.js | 4 +- .../public/autocomplete_providers/value.js | 35 +- .../plugins/ml/public/explorer/explorer.html | 8 +- .../ml/public/explorer/explorer_controller.js | 18 +- x-pack/plugins/ml/public/util/index_utils.js | 2 +- .../dashboard_mode/dashboard_view_mode.js | 5 +- 132 files changed, 3344 insertions(+), 4047 deletions(-) delete mode 100644 packages/kbn-es-query/README.md delete mode 100644 packages/kbn-es-query/src/filters/index.d.ts delete mode 100644 packages/kbn-es-query/src/filters/lib/custom_filter.ts delete mode 100644 packages/kbn-es-query/src/filters/lib/index.ts delete mode 100644 packages/kbn-es-query/src/filters/lib/meta_filter.ts delete mode 100644 packages/kbn-es-query/src/filters/lib/phrases_filter.ts delete mode 100644 packages/kbn-es-query/src/filters/lib/query_string_filter.ts rename packages/kbn-es-query/src/filters/lib/geo_bounding_box_filter.ts => src/fixtures/filters/exists_filter.js (74%) rename packages/kbn-es-query/src/filters/lib/phrase_filter.ts => src/fixtures/filters/index.js (75%) rename src/{ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts => fixtures/filters/phrase_filter.js} (80%) rename src/{ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts => fixtures/filters/phrases_filter.js} (70%) rename src/{ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts => fixtures/filters/range_filter.js} (72%) rename src/{ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts => fixtures/filters/scripted_phrase_filter.js} (60%) delete mode 100644 src/ui/public/apply_filters/apply_filters_popover.tsx delete mode 100644 src/ui/public/apply_filters/directive.html delete mode 100644 src/ui/public/apply_filters/directive.js create mode 100644 src/ui/public/filter_bar/__tests__/filter_bar.js create mode 100644 src/ui/public/filter_bar/__tests__/filter_bar_click_handler.js delete mode 100644 src/ui/public/filter_bar/_global_filter_group.scss delete mode 100644 src/ui/public/filter_bar/_global_filter_item.scss delete mode 100644 src/ui/public/filter_bar/_index.scss create mode 100644 src/ui/public/filter_bar/filter_bar.html create mode 100644 src/ui/public/filter_bar/filter_bar.js delete mode 100644 src/ui/public/filter_bar/filter_bar.tsx create mode 100644 src/ui/public/filter_bar/filter_bar_click_handler.js delete mode 100644 src/ui/public/filter_bar/filter_editor/generic_combo_box.tsx delete mode 100644 src/ui/public/filter_bar/filter_editor/index.tsx delete mode 100644 src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts delete mode 100644 src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.ts delete mode 100644 src/ui/public/filter_bar/filter_editor/lib/filter_operators.ts delete mode 100644 src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx delete mode 100644 src/ui/public/filter_bar/filter_editor/phrase_value_input.tsx delete mode 100644 src/ui/public/filter_bar/filter_editor/phrases_values_input.tsx delete mode 100644 src/ui/public/filter_bar/filter_editor/range_value_input.tsx delete mode 100644 src/ui/public/filter_bar/filter_editor/value_input_type.tsx delete mode 100644 src/ui/public/filter_bar/filter_item.tsx create mode 100644 src/ui/public/filter_bar/filter_pill/filter_pill.html create mode 100644 src/ui/public/filter_bar/filter_pill/filter_pill.js rename src/ui/public/{search_bar/components/index.tsx => filter_bar/filter_pill/index.js} (95%) delete mode 100644 src/ui/public/filter_bar/filter_view/index.tsx rename src/ui/public/{search_bar/index.tsx => filter_bar/index.js} (86%) delete mode 100644 src/ui/public/filter_bar/index.ts create mode 100644 src/ui/public/filter_bar/lib/__tests__/disable_filter.js rename packages/kbn-es-query/src/filters/lib/exists_filter.ts => src/ui/public/filter_bar/lib/__tests__/filter_applied_and_unwrap.js (56%) create mode 100644 src/ui/public/filter_bar/lib/__tests__/filter_out_time_based_filter.js create mode 100644 src/ui/public/filter_bar/lib/disable_filter.js rename src/ui/public/{apply_filters/index.ts => filter_bar/lib/filter_applied_and_unwrap.js} (87%) rename packages/kbn-es-query/src/filters/lib/range_filter.ts => src/ui/public/filter_bar/lib/filter_out_time_based_filter.js (64%) create mode 100644 src/ui/public/filter_editor/filter_editor.html create mode 100644 src/ui/public/filter_editor/filter_editor.js create mode 100644 src/ui/public/filter_editor/filter_editor.less create mode 100644 src/ui/public/filter_editor/filter_field_select.html create mode 100644 src/ui/public/filter_editor/filter_field_select.js create mode 100644 src/ui/public/filter_editor/filter_operator_select.html rename src/ui/public/{filter_bar/directive.js => filter_editor/filter_operator_select.js} (60%) create mode 100644 src/ui/public/filter_editor/filter_query_dsl_editor.html create mode 100644 src/ui/public/filter_editor/filter_query_dsl_editor.js rename src/ui/public/{documentation_links/index.d.ts => filter_editor/index.js} (94%) create mode 100644 src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js create mode 100644 src/ui/public/filter_editor/lib/filter_editor_utils.js create mode 100644 src/ui/public/filter_editor/lib/filter_operators.js create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_editor.html rename packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts => src/ui/public/filter_editor/params_editor/filter_params_editor.js (65%) create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_input_type.html create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_input_type.js create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js create mode 100644 src/ui/public/filter_editor/params_editor/filter_params_range_editor.html rename src/ui/public/{search_bar/directive/index.js => filter_editor/params_editor/filter_params_range_editor.js} (69%) delete mode 100644 src/ui/public/index_patterns/_field.d.ts delete mode 100644 src/ui/public/index_patterns/fixtures/index.ts delete mode 100644 src/ui/public/search_bar/components/filter_options.tsx delete mode 100644 src/ui/public/search_bar/components/search_bar.tsx delete mode 100644 src/ui/public/value_suggestions/index.ts delete mode 100644 src/ui/public/value_suggestions/value_suggestions.test.ts delete mode 100644 src/ui/public/value_suggestions/value_suggestions.ts diff --git a/packages/kbn-es-query/README.md b/packages/kbn-es-query/README.md deleted file mode 100644 index 53f082b864d732..00000000000000 --- a/packages/kbn-es-query/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# kbn-es-query - -This module is responsible for generating Elasticsearch queries for Kibana. See explanations below for each of the subdirectories. - -## es_query - -This folder contains the code that combines Lucene/KQL queries and filters into an Elasticsearch query. - -```javascript -buildEsQuery(indexPattern, queries, filters, config) -``` - -Generates the Elasticsearch query DSL from combining the queries and filters provided. - -```javascript -buildQueryFromFilters(filters, indexPattern) -``` - -Generates the Elasticsearch query DSL from the given filters. - -```javascript -luceneStringToDsl(query) -``` - -Generates the Elasticsearch query DSL from the given Lucene query. - -```javascript -migrateFilter(filter, indexPattern) -``` - -Migrates a filter from a previous version of Elasticsearch to the current version. - -```javascript -decorateQuery(query, queryStringOptions) -``` - -Decorates an Elasticsearch query_string query with the given options. - -## filters - -This folder contains the code related to Kibana Filter objects, including their definitions, and helper functions to create them. Filters in Kibana always contain a `meta` property which describes which `index` the filter corresponds to, as well as additional data about the specific filter. - -The object that is created by each of the following functions corresponds to a Filter object in the `lib` directory (e.g. `PhraseFilter`, `RangeFilter`, etc.) - -```javascript -buildExistsFilter(field, indexPattern) -``` - -Creates a filter (`ExistsFilter`) where the given field exists. - -```javascript -buildPhraseFilter(field, value, indexPattern) -``` - -Creates an filter (`PhraseFilter`) where the given field matches the given value. - -```javascript -buildPhrasesFilter(field, params, indexPattern) -``` - -Creates a filter (`PhrasesFilter`) where the given field matches one or more of the given values. `params` should be an array of values. - -```javascript -buildQueryFilter(query, index) -``` - -Creates a filter (`CustomFilter`) corresponding to a raw Elasticsearch query DSL object. - -```javascript -buildRangeFilter(field, params, indexPattern) -``` - -Creates a filter (`RangeFilter`) where the value for the given field is in the given range. `params` should contain `lt`, `lte`, `gt`, and/or `gte`. - -## kuery - -This folder contains the code corresponding to generating Elasticsearch queries using the Kibana query language. - -It also contains code corresponding to the original implementation of Kuery (released in 6.0) which should be removed at some point (see legacy_kuery.js, legacy_kuery.peg). - -In general, you will only need to worry about the following functions from the `ast` folder: - -```javascript -fromExpression(expression) -``` - -Generates an abstract syntax tree corresponding to the raw Kibana query `expression`. - -```javascript -toElasticsearchQuery(node, indexPattern) -``` - -Takes an abstract syntax tree (generated from the previous method) and generates the Elasticsearch query DSL using the given `indexPattern`. Note that if no `indexPattern` is provided, then an Elasticsearch query DSL will still be generated, ignoring things like the index pattern scripted fields, field types, etc. - diff --git a/packages/kbn-es-query/package.json b/packages/kbn-es-query/package.json index ec4226f6b1e758..ce3e96cc8219ca 100644 --- a/packages/kbn-es-query/package.json +++ b/packages/kbn-es-query/package.json @@ -5,15 +5,14 @@ "license": "Apache-2.0", "private": true, "scripts": { - "build": "tsc && babel src --out-dir target", + "build": "babel src --out-dir target", "kbn:bootstrap": "yarn build --quiet", "kbn:watch": "yarn build --watch" -}, + }, "dependencies": { "lodash": "npm:@elastic/lodash@3.10.1-kibana1" }, "devDependencies": { - "typescript": "^3.0.3", "@kbn/babel-preset": "1.0.0", "babel-cli": "^6.26.0", "expect.js": "0.3.1" diff --git a/packages/kbn-es-query/src/filters/exists.js b/packages/kbn-es-query/src/filters/exists.js index 0c82279fb44176..dd0d62f36c1461 100644 --- a/packages/kbn-es-query/src/filters/exists.js +++ b/packages/kbn-es-query/src/filters/exists.js @@ -17,7 +17,6 @@ * under the License. */ -// Creates a filter where the given field exists export function buildExistsFilter(field, indexPattern) { return { meta: { diff --git a/packages/kbn-es-query/src/filters/index.d.ts b/packages/kbn-es-query/src/filters/index.d.ts deleted file mode 100644 index c46a767e38ea4c..00000000000000 --- a/packages/kbn-es-query/src/filters/index.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { Field, IndexPattern } from 'ui/index_patterns'; -import { CustomFilter, ExistsFilter, PhraseFilter, PhrasesFilter, RangeFilter } from './lib'; -import { RangeFilterParams } from './lib/range_filter'; - -export * from './lib'; - -export function buildExistsFilter(field: Field, indexPattern: IndexPattern): ExistsFilter; - -export function buildPhraseFilter( - field: Field, - value: string, - indexPattern: IndexPattern -): PhraseFilter; - -export function buildPhrasesFilter( - field: Field, - values: string[], - indexPattern: IndexPattern -): PhrasesFilter; - -export function buildQueryFilter(query: any, index: string): CustomFilter; - -export function buildRangeFilter( - field: Field, - params: RangeFilterParams, - indexPattern: IndexPattern, - formattedValue?: string -): RangeFilter; diff --git a/packages/kbn-es-query/src/filters/index.js b/packages/kbn-es-query/src/filters/index.js index d7d092eabd8a2f..dfd89247368620 100644 --- a/packages/kbn-es-query/src/filters/index.js +++ b/packages/kbn-es-query/src/filters/index.js @@ -22,4 +22,3 @@ export * from './phrase'; export * from './phrases'; export * from './query'; export * from './range'; -export * from './lib'; diff --git a/packages/kbn-es-query/src/filters/lib/custom_filter.ts b/packages/kbn-es-query/src/filters/lib/custom_filter.ts deleted file mode 100644 index 1003cc984a90f4..00000000000000 --- a/packages/kbn-es-query/src/filters/lib/custom_filter.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 './meta_filter'; - -export type CustomFilter = Filter & { - query: any; -}; diff --git a/packages/kbn-es-query/src/filters/lib/index.ts b/packages/kbn-es-query/src/filters/lib/index.ts deleted file mode 100644 index fdf87c84eb5caf..00000000000000 --- a/packages/kbn-es-query/src/filters/lib/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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. - */ - -// The interface the other filters extend -export * from './meta_filter'; - -// The actual filter types -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 { QueryStringFilter } from './query_string_filter'; -import { RangeFilter } from './range_filter'; -export { - CustomFilter, - ExistsFilter, - GeoBoundingBoxFilter, - GeoPolygonFilter, - PhraseFilter, - PhrasesFilter, - QueryStringFilter, - 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/packages/kbn-es-query/src/filters/lib/meta_filter.ts b/packages/kbn-es-query/src/filters/lib/meta_filter.ts deleted file mode 100644 index db5b308a01da38..00000000000000 --- a/packages/kbn-es-query/src/filters/lib/meta_filter.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 enum FilterStateStore { - APP_STATE = 'appState', - GLOBAL_STATE = 'globalState', -} - -export interface FilterState { - store: FilterStateStore; -} - -export interface FilterMeta { - // 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; - key?: string; - value?: string; -} - -export interface Filter { - $state: FilterState; - meta: FilterMeta; - query?: any; -} - -export interface LatLon { - lat: number; - lon: number; -} - -export function buildEmptyFilter(isPinned: boolean, index?: string): Filter { - 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: Filter) { - return filter.$state.store === FilterStateStore.GLOBAL_STATE; -} - -export function toggleFilterDisabled(filter: Filter) { - const disabled = !filter.meta.disabled; - const meta = { ...filter.meta, disabled }; - return { ...filter, meta }; -} - -export function toggleFilterNegated(filter: Filter) { - const negate = !filter.meta.negate; - const meta = { ...filter.meta, negate }; - return { ...filter, meta }; -} - -export function toggleFilterPinned(filter: Filter) { - const store = isFilterPinned(filter) ? FilterStateStore.APP_STATE : FilterStateStore.GLOBAL_STATE; - const $state = { ...filter.$state, store }; - return { ...filter, $state }; -} - -export function enableFilter(filter: Filter) { - return !filter.meta.disabled ? filter : toggleFilterDisabled(filter); -} - -export function disableFilter(filter: Filter) { - return filter.meta.disabled ? filter : toggleFilterDisabled(filter); -} - -export function pinFilter(filter: Filter) { - return isFilterPinned(filter) ? filter : toggleFilterPinned(filter); -} - -export function unpinFilter(filter: Filter) { - return !isFilterPinned(filter) ? filter : toggleFilterPinned(filter); -} diff --git a/packages/kbn-es-query/src/filters/lib/phrases_filter.ts b/packages/kbn-es-query/src/filters/lib/phrases_filter.ts deleted file mode 100644 index cc36ad54c8dc4a..00000000000000 --- a/packages/kbn-es-query/src/filters/lib/phrases_filter.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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, FilterMeta } from './meta_filter'; - -export type PhrasesFilterMeta = FilterMeta & { - params: string[]; // The unformatted values -}; - -export type PhrasesFilter = Filter & { - meta: PhrasesFilterMeta; -}; diff --git a/packages/kbn-es-query/src/filters/lib/query_string_filter.ts b/packages/kbn-es-query/src/filters/lib/query_string_filter.ts deleted file mode 100644 index 1f6a95844437a6..00000000000000 --- a/packages/kbn-es-query/src/filters/lib/query_string_filter.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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, FilterMeta } from './meta_filter'; - -export type QueryStringFilterMeta = FilterMeta; - -export type QueryStringFilter = Filter & { - meta: QueryStringFilterMeta; -}; diff --git a/packages/kbn-es-query/src/filters/phrase.js b/packages/kbn-es-query/src/filters/phrase.js index b71724ab60d766..8677fde87ceda2 100644 --- a/packages/kbn-es-query/src/filters/phrase.js +++ b/packages/kbn-es-query/src/filters/phrase.js @@ -17,7 +17,6 @@ * under the License. */ -// Creates an filter where the given field matches the given value export function buildPhraseFilter(field, value, indexPattern) { const filter = { meta: { index: indexPattern.id } }; const convertedValue = getConvertedValueForField(field, value); diff --git a/packages/kbn-es-query/src/filters/phrases.js b/packages/kbn-es-query/src/filters/phrases.js index f02b3763f37bb7..0d85669e65fcde 100644 --- a/packages/kbn-es-query/src/filters/phrases.js +++ b/packages/kbn-es-query/src/filters/phrases.js @@ -19,8 +19,6 @@ import { getPhraseScript } from './phrase'; -// Creates a filter where the given field matches one or more of the given values -// params should be an array of values export function buildPhrasesFilter(field, params, indexPattern) { const index = indexPattern.id; const type = 'phrases'; diff --git a/packages/kbn-es-query/src/filters/query.js b/packages/kbn-es-query/src/filters/query.js index cfb1a6d36d9e2f..428d656364987c 100644 --- a/packages/kbn-es-query/src/filters/query.js +++ b/packages/kbn-es-query/src/filters/query.js @@ -17,7 +17,6 @@ * under the License. */ -// Creates a filter corresponding to a raw Elasticsearch query DSL object export function buildQueryFilter(query, index) { return { query: query, diff --git a/packages/kbn-es-query/src/filters/range.js b/packages/kbn-es-query/src/filters/range.js index 88be09c9971546..ec3fc167f75d4e 100644 --- a/packages/kbn-es-query/src/filters/range.js +++ b/packages/kbn-es-query/src/filters/range.js @@ -36,8 +36,6 @@ function formatValue(field, params) { return _.map(params, (val, key) => operators[key] + format(field, val)).join(' '); } -// Creates a filter where the value for the given field is in the given range -// params should be an object containing `lt`, `lte`, `gt`, and/or `gte` export function buildRangeFilter(field, params, indexPattern, formattedValue) { const filter = { meta: { index: indexPattern.id } }; if (formattedValue) filter.meta.formattedValue = formattedValue; diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts index 873636a28889fd..79e6903b186448 100644 --- a/packages/kbn-es-query/src/index.d.ts +++ b/packages/kbn-es-query/src/index.d.ts @@ -18,4 +18,3 @@ */ export * from './kuery'; -export * from './filters'; diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json index c2b6e3075dc675..9a22ea4fc44aa3 100644 --- a/packages/kbn-es-query/tsconfig.json +++ b/packages/kbn-es-query/tsconfig.json @@ -1,11 +1,7 @@ { - "extends": "../../tsconfig.browser.json", - "compilerOptions": { - "rootDir": "./src", - "outDir": "./target" - }, + "extends": "../../tsconfig.json", "include": [ "index.d.ts", - "src/**/*.ts" + "src/**/*.d.ts" ] } diff --git a/packages/kbn-es-query/src/filters/lib/geo_bounding_box_filter.ts b/src/fixtures/filters/exists_filter.js similarity index 74% rename from packages/kbn-es-query/src/filters/lib/geo_bounding_box_filter.ts rename to src/fixtures/filters/exists_filter.js index c83e146b093a35..0c4d6138a99b71 100644 --- a/packages/kbn-es-query/src/filters/lib/geo_bounding_box_filter.ts +++ b/src/fixtures/filters/exists_filter.js @@ -17,15 +17,19 @@ * under the License. */ -import { Filter, FilterMeta, LatLon } from './meta_filter'; - -export type GeoBoundingBoxFilterMeta = FilterMeta & { - params: { - bottom_right: LatLon; - top_left: LatLon; - }; -}; - -export type GeoBoundingBoxFilter = Filter & { - meta: GeoBoundingBoxFilterMeta; +export const existsFilter = { + 'meta': { + 'index': 'logstash-*', + 'negate': false, + 'disabled': false, + 'type': 'exists', + 'key': 'machine.os', + 'value': 'exists' + }, + 'exists': { + 'field': 'machine.os' + }, + '$state': { + 'store': 'appState' + } }; diff --git a/packages/kbn-es-query/src/filters/lib/phrase_filter.ts b/src/fixtures/filters/index.js similarity index 75% rename from packages/kbn-es-query/src/filters/lib/phrase_filter.ts rename to src/fixtures/filters/index.js index a8613190ce786b..e8f823c4e8cbe4 100644 --- a/packages/kbn-es-query/src/filters/lib/phrase_filter.ts +++ b/src/fixtures/filters/index.js @@ -17,14 +17,8 @@ * under the License. */ -import { Filter, FilterMeta } from './meta_filter'; - -export type PhraseFilterMeta = FilterMeta & { - params: { - query: string; // The unformatted value - }; -}; - -export type PhraseFilter = Filter & { - meta: PhraseFilterMeta; -}; +export { phraseFilter } from './phrase_filter'; +export { scriptedPhraseFilter } from './scripted_phrase_filter'; +export { phrasesFilter } from './phrases_filter'; +export { rangeFilter } from './range_filter'; +export { existsFilter } from './exists_filter'; diff --git a/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts b/src/fixtures/filters/phrase_filter.js similarity index 80% rename from src/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts rename to src/fixtures/filters/phrase_filter.js index 77bb8e06c801ad..22f02347d94aa6 100644 --- a/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts +++ b/src/fixtures/filters/phrase_filter.js @@ -17,22 +17,24 @@ * under the License. */ -import { FilterStateStore, PhraseFilter } from '@kbn/es-query'; - -export const phraseFilter: PhraseFilter = { +export const phraseFilter = { meta: { negate: false, index: 'logstash-*', type: 'phrase', key: 'machine.os', value: 'ios', - disabled: false, - alias: null, - params: { - query: 'ios', - }, + disabled: false }, - $state: { - store: FilterStateStore.APP_STATE, + query: { + match: { + 'machine.os': { + query: 'ios', + type: 'phrase' + } + } }, + $state: { + store: 'appState' + } }; diff --git a/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts b/src/fixtures/filters/phrases_filter.js similarity index 70% rename from src/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts rename to src/fixtures/filters/phrases_filter.js index e86c3ee1318e34..184f1268c9da0c 100644 --- a/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts +++ b/src/fixtures/filters/phrases_filter.js @@ -17,20 +17,37 @@ * under the License. */ -import { FilterStateStore, PhrasesFilter } from '@kbn/es-query'; - -export const phrasesFilter: PhrasesFilter = { +export const phrasesFilter = { meta: { index: 'logstash-*', type: 'phrases', key: 'machine.os.raw', value: 'win xp, osx', - params: ['win xp', 'osx'], + params: [ + 'win xp', + 'osx' + ], negate: false, - disabled: false, - alias: null, + disabled: false }, - $state: { - store: FilterStateStore.APP_STATE, + query: { + bool: { + should: [ + { + match_phrase: { + 'machine.os.raw': 'win xp' + } + }, + { + match_phrase: { + 'machine.os.raw': 'osx' + } + } + ], + minimum_should_match: 1 + } }, + $state: { + store: 'appState' + } }; diff --git a/src/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts b/src/fixtures/filters/range_filter.js similarity index 72% rename from src/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts rename to src/fixtures/filters/range_filter.js index a17f767006f3ea..c18b6c98a56397 100644 --- a/src/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts +++ b/src/fixtures/filters/range_filter.js @@ -17,18 +17,23 @@ * under the License. */ -import { ExistsFilter, FilterStateStore } from '@kbn/es-query'; - -export const existsFilter: ExistsFilter = { - meta: { - index: 'logstash-*', - negate: false, - disabled: false, - type: 'exists', - key: 'machine.os', - alias: null, +export const rangeFilter = { + 'meta': { + 'index': 'logstash-*', + 'negate': false, + 'disabled': false, + 'alias': null, + 'type': 'range', + 'key': 'bytes', + 'value': '0 to 10' }, - $state: { - store: FilterStateStore.APP_STATE, + 'range': { + 'bytes': { + 'gte': 0, + 'lt': 10 + } }, + '$state': { + 'store': 'appState' + } }; diff --git a/src/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts b/src/fixtures/filters/scripted_phrase_filter.js similarity index 60% rename from src/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts rename to src/fixtures/filters/scripted_phrase_filter.js index f6daf9cb36f117..f21a6356de19f0 100644 --- a/src/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts +++ b/src/fixtures/filters/scripted_phrase_filter.js @@ -17,23 +17,26 @@ * under the License. */ -import { FilterStateStore, RangeFilter } from '@kbn/es-query'; - -export const rangeFilter: RangeFilter = { - meta: { - index: 'logstash-*', - negate: false, - disabled: false, - alias: null, - type: 'range', - key: 'bytes', - value: '0 to 10', - params: { - gte: 0, - lt: 10, - }, +export const scriptedPhraseFilter = { + 'meta': { + 'negate': false, + 'index': 'logstash-*', + 'field': 'script string', + 'type': 'phrase', + 'key': 'script string', + 'value': 'i am a string', + 'disabled': false }, - $state: { - store: FilterStateStore.APP_STATE, + 'script': { + 'script': { + 'inline': 'boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { \'i am a string\' }, params.value);', + 'lang': 'painless', + 'params': { + 'value': 'i am a string' + } + } }, + '$state': { + 'store': 'appState' + } }; diff --git a/src/legacy/core_plugins/kibana/public/context/app.html b/src/legacy/core_plugins/kibana/public/context/app.html index ea9b66ac29e56b..9bdd538b850bb8 100644 --- a/src/legacy/core_plugins/kibana/public/context/app.html +++ b/src/legacy/core_plugins/kibana/public/context/app.html @@ -19,12 +19,7 @@ - +
(predecessorCount) => ( @@ -67,10 +65,6 @@ export function QueryParameterActionsProvider(indexPatterns, Private) { ) ); - const updateFilters = () => filters => { - queryFilter.setFilters(filters); - }; - const addFilter = (state) => async (field, values, operation) => { const indexPatternId = state.queryParameters.indexPatternId; filterManager.add(field, values, operation, indexPatternId); @@ -80,7 +74,6 @@ export function QueryParameterActionsProvider(indexPatterns, Private) { return { addFilter, - updateFilters, increasePredecessorCount, increaseSuccessorCount, setPredecessorCount, 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 67774b34008168..0aa3c6eb930ccf 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -30,25 +30,22 @@
- + >
- + +
0) { - $scope.indexPatterns = panelIndexPatterns; - } - else { - indexPatterns.getDefault().then((defaultIndexPattern) => { - $scope.$evalAsync(() => { - $scope.indexPatterns = [defaultIndexPattern]; - }); - }); - } + $scope.indexPatterns = dashboardStateManager.getPanelIndexPatterns(); }; // Part of the exposed plugin API - do not remove without careful consideration. @@ -167,7 +153,7 @@ app.directive('dashboardApp', function ($injector) { query: '', language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage') }, - queryFilter.getFilters() + filterBar.getFilters() ); timefilter.enableAutoRefreshSelector(); @@ -238,31 +224,11 @@ app.directive('dashboardApp', function ($injector) { dashboardStateManager.requestReload(); } else { $scope.model.query = migrateLegacyQuery(query); - dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + dashboardStateManager.applyFilters($scope.model.query, filterBar.getFilters()); } $scope.refresh(); }; - $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); - }; - - $scope.onCancelApplyFilters = () => { - $scope.appState.$newFilters = []; - }; - - $scope.onApplyFilters = filters => { - queryFilter.addFiltersAndChangeTimeFilter(filters); - $scope.appState.$newFilters = []; - }; - - $scope.$watch('appState.$newFilters', (filters = []) => { - if (filters.length === 1) { - $scope.onApplyFilters(filters); - } - }); - $scope.indexPatterns = []; $scope.onPanelRemoved = (panelIndex) => { @@ -381,7 +347,7 @@ app.directive('dashboardApp', function ($injector) { }); } - $scope.showFilterBar = () => $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); + $scope.showFilterBar = () => filterBar.getFilters().length > 0 || !dashboardStateManager.getFullScreenMode(); $scope.showAddPanel = () => { dashboardStateManager.setFullScreenMode(false); @@ -492,13 +458,12 @@ app.directive('dashboardApp', function ($injector) { updateViewMode(dashboardStateManager.getViewMode()); // update root source when filters update - $scope.$listen(queryFilter, 'update', function () { - $scope.model.filters = queryFilter.getFilters(); - dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); + $scope.$listen(filterBar, 'update', function () { + dashboardStateManager.applyFilters($scope.model.query, filterBar.getFilters()); }); // update data when filters fire fetch event - $scope.$listen(queryFilter, 'fetch', $scope.refresh); + $scope.$listen(filterBar, 'fetch', $scope.refresh); $scope.$on('$destroy', () => { dashboardStateManager.destroy(); 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 5845625163cd3b..cedaa5fbb232aa 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/search_bar'; +import 'ui/query_bar'; import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; import { toastNotifications } from 'ui/notify'; import { VisProvider } from 'ui/vis'; @@ -351,24 +351,6 @@ 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); - }; - - $scope.applyFilters = filters => { - queryFilter.addFiltersAndChangeTimeFilter(filters); - $scope.state.$newFilters = []; - }; - - $scope.$watch('state.$newFilters', (filters = []) => { - if (filters.length === 1) { - $scope.applyFilters(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 @@ -506,7 +488,6 @@ 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 b11097a07ce023..f9fae245a64693 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -30,20 +30,23 @@

- + >

+
+ +
- + - -
{ - // The filters will automatically be set when the queryFilter emits an update event (see below) - 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); - } - }); - function init() { // export some objects $scope.savedVis = savedVis; @@ -376,7 +353,6 @@ 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/_index.scss b/src/ui/public/_index.scss index 88726b1a69e5e0..fc8e02aadc8da7 100644 --- a/src/ui/public/_index.scss +++ b/src/ui/public/_index.scss @@ -26,7 +26,6 @@ @import './notify/index'; @import './partials/index'; @import './query_bar/index'; -@import './filter_bar/index'; @import './style_compile/index'; // The following are prefixed with "vis" diff --git a/src/ui/public/agg_types/filter/agg_type_filters.test.ts b/src/ui/public/agg_types/filter/agg_type_filters.test.ts index 416e9c1b45fd2c..47e7bebf9f3652 100644 --- a/src/ui/public/agg_types/filter/agg_type_filters.test.ts +++ b/src/ui/public/agg_types/filter/agg_type_filters.test.ts @@ -21,7 +21,7 @@ import { AggTypeFilters } from './agg_type_filters'; describe('AggTypeFilters', () => { let registry: AggTypeFilters; - const indexPattern = { id: '1234', fields: [], title: 'foo' }; + const indexPattern = {}; const aggConfig = {}; beforeEach(() => { diff --git a/src/ui/public/apply_filters/apply_filters_popover.tsx b/src/ui/public/apply_filters/apply_filters_popover.tsx deleted file mode 100644 index 67f78b0951f191..00000000000000 --- a/src/ui/public/apply_filters/apply_filters_popover.tsx +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 { - EuiButton, - EuiButtonEmpty, - EuiForm, - EuiFormRow, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, - EuiSwitch, -} from '@elastic/eui'; -import { Filter } from '@kbn/es-query'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { Component } from 'react'; -import { getFilterDisplayText } from '../filter_bar/filter_view'; - -interface Props { - filters: Filter[]; - onCancel: () => void; - onSubmit: (filters: Filter[]) => void; -} - -interface State { - isFilterSelected: boolean[]; -} - -export class ApplyFiltersPopover extends Component { - public static defaultProps = { - filters: [], - }; - - public constructor(props: Props) { - super(props); - this.state = { - isFilterSelected: props.filters.map(() => true), - }; - } - - public render() { - if (this.props.filters.length === 0) { - return ''; - } - - const form = ( - - {this.props.filters.map((filter, i) => ( - - this.toggleFilterSelected(i)} - /> - - ))} - - ); - - return ( - - - - - - - - - {form} - - - - - - - - - - - - ); - } - - private isFilterSelected = (i: number) => { - return this.state.isFilterSelected[i]; - }; - - private toggleFilterSelected = (i: number) => { - const isFilterSelected = [...this.state.isFilterSelected]; - isFilterSelected[i] = !isFilterSelected[i]; - this.setState({ isFilterSelected }); - }; - - private onSubmit = () => { - const selectedFilters = this.props.filters.filter( - (filter, i) => this.state.isFilterSelected[i] - ); - this.props.onSubmit(selectedFilters); - }; -} diff --git a/src/ui/public/apply_filters/directive.html b/src/ui/public/apply_filters/directive.html deleted file mode 100644 index ed7a5d70a2b80b..00000000000000 --- a/src/ui/public/apply_filters/directive.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/ui/public/apply_filters/directive.js b/src/ui/public/apply_filters/directive.js deleted file mode 100644 index d364a1843494a4..00000000000000 --- a/src/ui/public/apply_filters/directive.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 template from './directive.html'; -import { ApplyFiltersPopover } from './apply_filters_popover'; -import { FilterBarLibMapAndFlattenFiltersProvider } from '../filter_bar/lib/map_and_flatten_filters'; - -const app = uiModules.get('app/kibana', ['react']); - -app.directive('applyFiltersPopoverComponent', (reactDirective) => { - return reactDirective(ApplyFiltersPopover); -}); - -app.directive('applyFiltersPopover', (reactDirective, Private) => { - const mapAndFlattenFilters = Private(FilterBarLibMapAndFlattenFiltersProvider); - - return { - template, - restrict: 'E', - scope: { - filters: '=', - onCancel: '=', - onSubmit: '=', - }, - link: function ($scope) { - $scope.state = {}; - - // Each time the new filters change we want to rebuild (not just re-render) the "apply filters" - // popover, because it has to reset its state whenever the new filters change. Setting a `key` - // property on the component accomplishes this due to how React handles the `key` property. - $scope.$watch('filters', filters => { - mapAndFlattenFilters(filters).then(mappedFilters => { - $scope.state = { - filters: mappedFilters, - key: Date.now(), - }; - }); - }); - } - }; -}); diff --git a/src/ui/public/doc_table/components/table_row.js b/src/ui/public/doc_table/components/table_row.js index f16be56778355d..b7c5768bbaa9a2 100644 --- a/src/ui/public/doc_table/components/table_row.js +++ b/src/ui/public/doc_table/components/table_row.js @@ -27,7 +27,7 @@ import { noWhiteSpace } from '../../../../legacy/core_plugins/kibana/common/util import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; import { uiModules } from '../../modules'; -import { disableFilter } from '@kbn/es-query'; +import { disableFilter } from '../../filter_bar'; import { dispatchRenderComplete } from '../../render_complete'; const module = uiModules.get('app/discover'); diff --git a/src/ui/public/filter_bar/__tests__/filter_bar.js b/src/ui/public/filter_bar/__tests__/filter_bar.js new file mode 100644 index 00000000000000..a17875a3d88376 --- /dev/null +++ b/src/ui/public/filter_bar/__tests__/filter_bar.js @@ -0,0 +1,228 @@ +/* + * 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 _ from 'lodash'; +import ngMock from 'ng_mock'; +import expect from 'expect.js'; +import sinon from 'sinon'; + +import MockState from 'fixtures/mock_state'; +import $ from 'jquery'; +import '..'; +import { FilterBarLibMapFilterProvider } from '../lib/map_filter'; +import { FilterBarQueryFilterProvider } from '../query_filter'; + +describe('Filter Bar Directive', function () { + let $rootScope; + let $compile; + let Promise; + let appState; + let mapFilter; + let $el; + let $scope; + + beforeEach(ngMock.module('kibana/global_state', function ($provide) { + $provide.service('getAppState', _.constant(_.constant( + appState = new MockState({ filters: [] }) + ))); + })); + + beforeEach(function () { + // load the application + ngMock.module('kibana'); + + ngMock.module('kibana/courier', function ($provide) { + $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); + }); + + ngMock.inject(function (Private, $injector, _$rootScope_, _$compile_) { + $rootScope = _$rootScope_; + $compile = _$compile_; + Promise = $injector.get('Promise'); + mapFilter = Private(FilterBarLibMapFilterProvider); + + const queryFilter = Private(FilterBarQueryFilterProvider); + queryFilter.getFilters = function () { + return appState.filters; + }; + }); + }); + + describe('Element rendering', function () { + beforeEach(function (done) { + const filters = [ + { meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'apache' } } } }, + { meta: { index: 'logstash-*' }, query: { match: { '_type': { query: 'nginx' } } } }, + { meta: { index: 'logstash-*' }, exists: { field: '@timestamp' } }, + { meta: { index: 'logstash-*' }, missing: { field: 'host' }, disabled: true }, + { meta: { index: 'logstash-*', alias: 'foo' }, query: { match: { '_type': { query: 'nginx' } } } }, + ]; + + Promise.map(filters, mapFilter).then(function (filters) { + appState.filters = filters; + $el = $compile('')($rootScope); + $scope = $el.isolateScope(); + }); + + const off = $rootScope.$on('filterbar:updated', function () { + off(); + // force a nextTick so it continues *after* the $digest loop completes + setTimeout(done, 0); + }); + + // kick off the digest loop + $rootScope.$digest(); + }); + + it('should render all the filters in state', function () { + const filters = $el.find('.filter'); + expect(filters).to.have.length(5); + expect($(filters[0]).find('span')[0].innerHTML).to.equal('_type:'); + expect($(filters[0]).find('span')[1].innerHTML).to.equal('"apache"'); + expect($(filters[1]).find('span')[0].innerHTML).to.equal('_type:'); + expect($(filters[1]).find('span')[1].innerHTML).to.equal('"nginx"'); + expect($(filters[2]).find('span')[0].innerHTML).to.equal('@timestamp:'); + expect($(filters[2]).find('span')[1].innerHTML).to.equal('"exists"'); + expect($(filters[3]).find('span')[0].innerHTML).to.equal('host:'); + expect($(filters[3]).find('span')[1].innerHTML).to.equal('"missing"'); + }); + + it('should be able to set an alias', function () { + const filter = $el.find('.filter')[4]; + expect($(filter).find('span')[0].innerHTML).to.equal('foo'); + }); + + describe('editing filters', function () { + beforeEach(function () { + $scope.editFilter(appState.filters[3]); + $scope.$digest(); + }); + + it('should be able to edit a filter', function () { + expect($el.find('.filter-edit-container').length).to.be(1); + }); + + it('should be able to stop editing a filter', function () { + $scope.cancelEdit(); + $scope.$digest(); + expect($el.find('.filter-edit-container').length).to.be(0); + }); + + it('should remove old filter and add new filter when saving', function () { + sinon.spy($scope, 'removeFilter'); + sinon.spy($scope, 'addFilters'); + + $scope.saveEdit(appState.filters[3], appState.filters[3], false); + expect($scope.removeFilter.called).to.be(true); + expect($scope.addFilters.called).to.be(true); + }); + }); + + describe('show and hide filters', function () { + let scope; + + beforeEach(() => { + scope = $rootScope.$new(); + }); + + function create(attrs) { + const template = ` +
+ + +
`; + + const element = $compile(template)(scope); + + scope.$apply(() => { + Object.assign(scope, attrs); + }); + + return element; + } + + + describe('collapse filters', function () { + let element; + + beforeEach(function () { + element = create({ + filterNavToggle: { + isOpen: false + } + }); + }); + + it('should be able to collapse filters', function () { + expect(element.hasClass('filter-panel-close')).to.be(true); + }); + + it('should be able to see `actions`', function () { + expect(element.find('.filter-link.pull-right').hasClass('action-show')).to.be(true); + }); + + it('should be able to view the same button for `expand`', function () { + expect(element.find('.filter-nav-link__icon').hasClass('filter-nav-link--close')).to.be(true); + }); + }); + + describe('expand filters', function () { + let element; + + beforeEach(function () { + element = create({ + filterNavToggle: { + isOpen: true + } + }); + }); + + it('should be able to expand filters', function () { + expect(element.hasClass('filter-panel-close')).to.be(false); + }); + + it('should be able to view the `actions` at the bottom of the filter-bar', function () { + expect(element.find('.filter-link.pull-right').hasClass('action-show')).to.be(false); + }); + + it('should be able to view the same button for `collapse`', function () { + expect(element.find('.filter-nav-link__icon').hasClass('filter-nav-link--close')).to.be(false); + }); + }); + + }); + + }); +}); diff --git a/src/ui/public/filter_bar/__tests__/filter_bar_click_handler.js b/src/ui/public/filter_bar/__tests__/filter_bar_click_handler.js new file mode 100644 index 00000000000000..612956327c18fe --- /dev/null +++ b/src/ui/public/filter_bar/__tests__/filter_bar_click_handler.js @@ -0,0 +1,80 @@ +/* + * 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 ngMock from 'ng_mock'; +import expect from 'expect.js'; + +import MockState from 'fixtures/mock_state'; +import { toastNotifications } from '../../notify'; +import AggConfigResult from '../../vis/agg_config_result'; + +import { VisProvider } from '../../vis'; +import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; +import { FilterBarClickHandlerProvider } from '../filter_bar_click_handler'; + +describe('filterBarClickHandler', function () { + let setup = null; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(function (Private) { + setup = function () { + const Vis = Private(VisProvider); + const createClickHandler = Private(FilterBarClickHandlerProvider); + const indexPattern = Private(StubbedLogstashIndexPatternProvider); + + const vis = new Vis(indexPattern, { + type: 'histogram', + aggs: [ + { type: 'count', schema: 'metric' }, + { + type: 'terms', + schema: 'segment', + params: { field: 'non-filterable' } + } + ] + }); + const aggConfigResult = new AggConfigResult(vis.aggs[1], void 0, 'apache', 'apache'); + + const $state = new MockState({ filters: [] }); + const clickHandler = createClickHandler($state); + + return { clickHandler, $state, aggConfigResult }; + }; + })); + + beforeEach(function () { + toastNotifications.list.splice(0); + }); + + describe('on non-filterable fields', function () { + it('warns about trying to filter on a non-filterable field', function () { + const { clickHandler, aggConfigResult } = setup(); + expect(toastNotifications.list).to.have.length(0); + clickHandler({ point: { aggConfigResult } }); + expect(toastNotifications.list).to.have.length(1); + }); + + it('does not warn if the event is click is being simulated', function () { + const { clickHandler, aggConfigResult } = setup(); + expect(toastNotifications.list).to.have.length(0); + clickHandler({ point: { aggConfigResult } }, true); + expect(toastNotifications.list).to.have.length(0); + }); + }); +}); diff --git a/src/ui/public/filter_bar/_global_filter_group.scss b/src/ui/public/filter_bar/_global_filter_group.scss deleted file mode 100644 index 9ed40964a95b4b..00000000000000 --- a/src/ui/public/filter_bar/_global_filter_group.scss +++ /dev/null @@ -1,22 +0,0 @@ -// SASSTODO: Probably not the right file for this selector, but temporary until the files get re-organized -.globalQueryBar { - padding-bottom: $euiSizeS; -} - -.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 { - line-height: 1; // Override kuiLocalNav & kuiLocalNavRow - overflow: hidden; - transition: height $euiAnimSpeedNormal $euiAnimSlightResistance; -} diff --git a/src/ui/public/filter_bar/_global_filter_item.scss b/src/ui/public/filter_bar/_global_filter_item.scss deleted file mode 100644 index b3bc510be0c746..00000000000000 --- a/src/ui/public/filter_bar/_global_filter_item.scss +++ /dev/null @@ -1,37 +0,0 @@ -@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; - } -} - -.globalFilterItem__editorForm { - padding: $euiSizeM; -} diff --git a/src/ui/public/filter_bar/_index.scss b/src/ui/public/filter_bar/_index.scss deleted file mode 100644 index 3c57b7fe2ca3ad..00000000000000 --- a/src/ui/public/filter_bar/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'global_filter_group'; -@import 'global_filter_item'; diff --git a/src/ui/public/filter_bar/filter_bar.html b/src/ui/public/filter_bar/filter_bar.html new file mode 100644 index 00000000000000..5cd239781d192c --- /dev/null +++ b/src/ui/public/filter_bar/filter_bar.html @@ -0,0 +1,149 @@ +
+
+
+
    +
  • Apply these filters?
  • +
  • + NOT {{ filter.meta.key }}: {{ filter.meta.value }} +
  • +
  • Change time to: {{changeTimeFilter.meta.value}}
  • +
  • +
    + + + +
    +
  • +
+
+
+ +
+
+ +
+ + + + + + + +
+ +
+
+ +
+
+ + + + + + + + +
+
+
diff --git a/src/ui/public/filter_bar/filter_bar.js b/src/ui/public/filter_bar/filter_bar.js new file mode 100644 index 00000000000000..0a1648ac75d4a9 --- /dev/null +++ b/src/ui/public/filter_bar/filter_bar.js @@ -0,0 +1,213 @@ +/* + * 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 _ from 'lodash'; +import template from './filter_bar.html'; +import '../directives/json_input'; +import '../filter_editor'; +import './filter_pill/filter_pill'; +import { filterAppliedAndUnwrap } from './lib/filter_applied_and_unwrap'; +import { FilterBarLibMapAndFlattenFiltersProvider } from './lib/map_and_flatten_filters'; +import { FilterBarLibMapFlattenAndWrapFiltersProvider } from './lib/map_flatten_and_wrap_filters'; +import { FilterBarLibExtractTimeFilterProvider } from './lib/extract_time_filter'; +import { FilterBarLibFilterOutTimeBasedFilterProvider } from './lib/filter_out_time_based_filter'; +import { changeTimeFilter } from './lib/change_time_filter'; +import { FilterBarQueryFilterProvider } from './query_filter'; +import { compareFilters } from './lib/compare_filters'; +import { uiModules } from '../modules'; + +export { disableFilter, enableFilter, toggleFilterDisabled } from './lib/disable_filter'; + + +const module = uiModules.get('kibana'); + +module.directive('filterBar', function (Private, Promise, getAppState) { + const mapAndFlattenFilters = Private(FilterBarLibMapAndFlattenFiltersProvider); + const mapFlattenAndWrapFilters = Private(FilterBarLibMapFlattenAndWrapFiltersProvider); + const extractTimeFilter = Private(FilterBarLibExtractTimeFilterProvider); + const filterOutTimeBasedFilter = Private(FilterBarLibFilterOutTimeBasedFilterProvider); + const queryFilter = Private(FilterBarQueryFilterProvider); + + return { + template, + restrict: 'E', + scope: { + indexPatterns: '=', + tooltipContent: '=', + }, + link: function ($scope, $elem) { + // bind query filter actions to the scope + [ + 'addFilters', + 'toggleFilter', + 'toggleAll', + 'pinFilter', + 'pinAll', + 'invertFilter', + 'invertAll', + 'removeFilter', + 'removeAll' + ].forEach(function (method) { + $scope[method] = queryFilter[method]; + }); + + $scope.state = getAppState(); + + $scope.showCollapseLink = () => { + const pill = $elem.find('filter-pill'); + return pill[pill.length - 1].offsetTop > 10; + }; + + $scope.filterNavToggle = { + isOpen: true, + tooltipContent: 'Collapse filter bar \n to show less' + }; + + $scope.toggleFilterShown = () => { + const collapser = $elem.find('.filter-nav-link__collapser'); + const filterPanelPill = $elem.find('.filter-panel__pill'); + if ($scope.filterNavToggle.isOpen) { + $scope.filterNavToggle.tooltipContent = 'Expand filter bar \n to show more'; + collapser.attr('aria-expanded', 'false'); + filterPanelPill.attr('style', 'width: calc(100% - 80px)'); + } else { + $scope.filterNavToggle.tooltipContent = 'Collapse filter bar \n to show less'; + collapser.attr('aria-expanded', 'true'); + filterPanelPill.attr('style', 'width: auto'); + } + + $scope.filterNavToggle.isOpen = !$scope.filterNavToggle.isOpen; + }; + + $scope.applyFilters = function (filters) { + addAndInvertFilters(filterAppliedAndUnwrap(filters)); + $scope.newFilters = []; + + // change time filter + if ($scope.changeTimeFilter && $scope.changeTimeFilter.meta && $scope.changeTimeFilter.meta.apply) { + changeTimeFilter($scope.changeTimeFilter); + } + }; + + $scope.addFilter = () => { + $scope.editingFilter = { + meta: { isNew: true } + }; + }; + + $scope.deleteFilter = (filter) => { + $scope.removeFilter(filter); + if (filter === $scope.editingFilter) $scope.cancelEdit(); + }; + + $scope.editFilter = (filter) => { + $scope.editingFilter = filter; + }; + + $scope.cancelEdit = () => { + delete $scope.editingFilter; + }; + + $scope.saveEdit = (filter, newFilter, isPinned) => { + if (!filter.meta.isNew) $scope.removeFilter(filter); + delete $scope.editingFilter; + $scope.addFilters([newFilter], isPinned); + }; + + $scope.clearFilterBar = function () { + $scope.newFilters = []; + $scope.changeTimeFilter = null; + }; + + // update the scope filter list on filter changes + $scope.$listen(queryFilter, 'update', function () { + updateFilters(); + }); + + // when appState changes, update scope's state + $scope.$watch(getAppState, function (appState) { + $scope.state = appState; + }); + + $scope.$watch('state.$newFilters', function (filters) { + if (!filters) return; + + // If filters is not undefined and the length is greater than + // one we need to set the newFilters attribute and allow the + // users to decide what they want to apply. + if (filters.length > 1) { + return mapFlattenAndWrapFilters(filters) + .then(function (results) { + extractTimeFilter(results).then(function (filter) { + $scope.changeTimeFilter = filter; + }); + return results; + }) + .then(filterOutTimeBasedFilter) + .then(function (results) { + $scope.newFilters = results; + }); + } + + // Just add single filters to the state. + if (filters.length === 1) { + Promise.resolve(filters).then(function (filters) { + extractTimeFilter(filters) + .then(function (timeFilter) { + if (timeFilter) changeTimeFilter(timeFilter); + }); + return filters; + }) + .then(filterOutTimeBasedFilter) + .then(addAndInvertFilters); + } + }); + + function addAndInvertFilters(filters) { + const existingFilters = queryFilter.getFilters(); + const inversionFilters = _.filter(existingFilters, (existingFilter) => { + const newMatchingFilter = _.find(filters, _.partial(compareFilters, existingFilter)); + return newMatchingFilter + && newMatchingFilter.meta + && existingFilter.meta + && existingFilter.meta.negate !== newMatchingFilter.meta.negate; + }); + const newFilters = _.reject(filters, (filter) => { + return _.find(inversionFilters, _.partial(compareFilters, filter)); + }); + + _.forEach(inversionFilters, $scope.invertFilter); + $scope.addFilters(newFilters); + } + + function updateFilters() { + const filters = queryFilter.getFilters(); + mapAndFlattenFilters(filters).then(function (results) { + // used to display the current filters in the state + $scope.filters = _.sortBy(results, function (filter) { + return !filter.meta.pinned; + }); + $scope.$emit('filterbar:updated'); + }); + } + + updateFilters(); + } + }; +}); diff --git a/src/ui/public/filter_bar/filter_bar.less b/src/ui/public/filter_bar/filter_bar.less index e69de29bb2d1d6..979ca42ea5dc1b 100644 --- a/src/ui/public/filter_bar/filter_bar.less +++ b/src/ui/public/filter_bar/filter_bar.less @@ -0,0 +1,226 @@ +// Variables ================================================================== +@filter-bar-confirm-bg: @gray-lighter; +@filter-bar-confirm-filter-color: @gray-darker; +@filter-bar-confirm-border: @gray-light; +@filter-bar-confirm-filter-bg: @gray-light; + +@filter-bar-bar-bg: @gray-lightest; +@filter-bar-bar-border: @gray-lighter; +@filter-bar-bar-condensed-bg: tint(@blue, 90%); + +@filter-bar-bar-filter-bg: @blue; +@filter-bar-bar-filter-color: @white; +@filter-bar-bar-filter-negate-bg: @brand-danger; +@filterBarDepth: 4; + +filter-bar { + z-index: @filterBarDepth !important; +} + +.filter-bar-confirm { + padding: 8px 10px 4px; + background: @filter-bar-confirm-bg; + border-bottom: 1px solid; + border-bottom-color: @filter-bar-confirm-border; + + ul { + margin-bottom: 0px; + } + + li { + display: inline-block; + } + + li:first-child { + font-weight: bold; + font-size: 1.2em; + } + + li button { + font-size: 0.9em; + padding: 2px 8px; + } + + .filter { + position: relative; + display: inline-block; + text-align: center; + // Number of filter icons multiplied by icon width + // Escaped to prevent less math + min-width: ~"calc(5*(1.414em + 13px))"; + vertical-align: middle; + font-size: @font-size-small; + background-color: @filter-bar-confirm-filter-bg; + color: @filter-bar-confirm-filter-color; + margin-right: 4px; + margin-bottom: 4px; + max-width: 100%; + + // Replace padding with border so absolute controls position correctly + padding: 4px 8px; + border-radius: 12px; + } +} + +.filter-panel { + position: relative; + + .filter-panel__pill { + display: inline; + } + +} + +.filter-panel--close { + max-height: 36px; + overflow-y: hidden; + min-width: 250px; + + .filter-panel__pill { + display: inline-block; + } +} + +.filter-bar { + padding: 6px 10px 1px 10px; + background: @filter-bar-bar-bg; + border-bottom: solid 1px @gray-lighter; + + .ace_editor { + height: 175px; + } + + .filter-edit-alias { + margin-top: 15px; + } + + .filter-link { + position: relative; + display: inline-block; + border: 4px solid transparent; + margin-bottom: 4px; + } + + .filter-description { + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; + line-height: 1.5; + } + + .action-show { + position: absolute; + right: 30px; + bottom: 0; + } + + .filter-nav-link__icon { + display: inline; + position: absolute; + top: 10px; + right: 10px; + opacity: 0.75; + font-size: 16px; + + &:hover { + opacity: 1; + } + + .filter-nav-link__collapser { + border: none; + line-height: 1; + } + } + + .filter { + position: relative; + display: inline-block; + text-align: center; + // Number of filter icons multiplied by icon width + // Escaped to prevent less math + min-width: ~"calc(5*(1.414em + 13px))"; + font-size: @font-size-small; + background-color: @filter-bar-bar-filter-bg; + color: @filter-bar-bar-filter-color; + margin-right: 4px; + margin-bottom: 4px; + max-width: 100%; + vertical-align: middle; + + // Replace padding with border so absolute controls position correctly + padding: 4px 8px; + border-radius: 12px; + + .filter-actions { + font-size: 1.1em; + line-height: 1.4em; + position: absolute; + padding: 4px 8px; + top: 0; + left: 0; + width: 100%; + opacity: 0; + text-align: center; + white-space: nowrap; + display: flex; + + .action { + border: none; + border-right: 1px solid rgba(255, 255, 255, 0.4); + padding: 0; + background-color: transparent; + flex: 1 1 auto; + + &:last-child { + border-right: 0; + padding-right: 0; + margin-right: 0; + } + + .unpinned { + .opacity(.7); + } + + .fa-disabled { + opacity: 0.7; + cursor: not-allowed; + } + } + } + + .filter-actions-activated { + opacity: 1; + } + + .filter-description-deactivated { + opacity: 0.15; + background: transparent; + overflow: hidden; + } + + &.negate { + background-color: @filter-bar-bar-filter-negate-bg; + } + + a { + color: @filter-bar-bar-filter-color; + } + + &.disabled { + opacity: 0.6; + background-image: repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(255,255,255,.3) 10px, rgba(255,255,255,.3) 20px); + } + + &.disabled:hover { + span { + text-decoration: none; + } + } + } +} + + .filter-bar-condensed { + padding: 6px 6px 2px 6px !important; + font-size: 0.9em; + background: @filter-bar-bar-condensed-bg; + } diff --git a/src/ui/public/filter_bar/filter_bar.tsx b/src/ui/public/filter_bar/filter_bar.tsx deleted file mode 100644 index 551114e0bf33aa..00000000000000 --- a/src/ui/public/filter_bar/filter_bar.tsx +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui'; -import { - buildEmptyFilter, - disableFilter, - enableFilter, - Filter, - pinFilter, - toggleFilterDisabled, - toggleFilterNegated, - unpinFilter, -} from '@kbn/es-query'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import classNames from 'classnames'; -import React, { Component } from 'react'; -import chrome from 'ui/chrome'; -import { IndexPattern } from 'ui/index_patterns'; -import { FilterOptions } from 'ui/search_bar/components/filter_options'; -import { FilterEditor } from './filter_editor'; -import { FilterItem } from './filter_item'; - -const config = chrome.getUiSettingsClient(); - -interface Props { - filters: Filter[]; - onFiltersUpdated: (filters: Filter[]) => void; - className: string; - indexPatterns: IndexPattern[]; - intl: InjectedIntl; -} - -interface State { - isAddFilterPopoverOpen: boolean; -} - -class FilterBarUI extends Component { - public state = { - isAddFilterPopoverOpen: false, - }; - - public render() { - const classes = classNames('globalFilterBar', this.props.className); - - return ( - - - - - - - - {this.renderItems()} - {this.renderAddFilter()} - - - - ); - } - - private renderItems() { - return this.props.filters.map((filter, i) => ( - - this.onUpdate(i, newFilter)} - onRemove={() => this.onRemove(i)} - indexPatterns={this.props.indexPatterns} - /> - - )); - } - - 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 = ( - - +{' '} - - - ); - - return ( - - -
- -
-
-
- ); - } - - private onAdd = (filter: Filter) => { - this.onCloseAddFilterPopover(); - const filters = [...this.props.filters, filter]; - this.props.onFiltersUpdated(filters); - }; - - private onRemove = (i: number) => { - const filters = [...this.props.filters]; - filters.splice(i, 1); - this.props.onFiltersUpdated(filters); - }; - - private onUpdate = (i: number, filter: Filter) => { - const filters = [...this.props.filters]; - filters[i] = filter; - this.props.onFiltersUpdated(filters); - }; - - private onEnableAll = () => { - const filters = this.props.filters.map(enableFilter); - this.props.onFiltersUpdated(filters); - }; - - private onDisableAll = () => { - const filters = this.props.filters.map(disableFilter); - this.props.onFiltersUpdated(filters); - }; - - private onPinAll = () => { - const filters = this.props.filters.map(pinFilter); - this.props.onFiltersUpdated(filters); - }; - - private onUnpinAll = () => { - const filters = this.props.filters.map(unpinFilter); - this.props.onFiltersUpdated(filters); - }; - - private onToggleAllNegated = () => { - const filters = this.props.filters.map(toggleFilterNegated); - this.props.onFiltersUpdated(filters); - }; - - private onToggleAllDisabled = () => { - const filters = this.props.filters.map(toggleFilterDisabled); - this.props.onFiltersUpdated(filters); - }; - - private onRemoveAll = () => { - this.props.onFiltersUpdated([]); - }; - - private onOpenAddFilterPopover = () => { - this.setState({ - isAddFilterPopoverOpen: true, - }); - }; - - private onCloseAddFilterPopover = () => { - this.setState({ - isAddFilterPopoverOpen: false, - }); - }; -} - -export const FilterBar = injectI18n(FilterBarUI); diff --git a/src/ui/public/filter_bar/filter_bar_click_handler.js b/src/ui/public/filter_bar/filter_bar_click_handler.js new file mode 100644 index 00000000000000..060be79644d381 --- /dev/null +++ b/src/ui/public/filter_bar/filter_bar_click_handler.js @@ -0,0 +1,89 @@ +/* + * 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 _ from 'lodash'; +import { dedupFilters } from './lib/dedup_filters'; +import { uniqFilters } from './lib/uniq_filters'; +import { findByParam } from '../utils/find_by_param'; +import { toastNotifications } from '../notify'; + +export function FilterBarClickHandlerProvider() { + + return function ($state) { + return function (event, simulate) { + if (!$state) return; + + let aggConfigResult; + + // Hierarchical and tabular data set their aggConfigResult parameter + // differently because of how the point is rewritten between the two. So + // we need to check if the point.orig is set, if not use try the point.aggConfigResult + if (event.point.orig) { + aggConfigResult = event.point.orig.aggConfigResult; + } else if (event.point.values) { + aggConfigResult = findByParam(event.point.values, 'aggConfigResult'); + } else { + aggConfigResult = event.point.aggConfigResult; + } + + if (aggConfigResult) { + const isLegendLabel = !!event.point.values; + let aggBuckets = _.filter(aggConfigResult.getPath(), { type: 'bucket' }); + + // For legend clicks, use the last bucket in the path + if (isLegendLabel) { + // series data has multiple values, use aggConfig on the first + // hierarchical data values is an object with the addConfig + const aggConfig = findByParam(event.point.values, 'aggConfig'); + aggBuckets = aggBuckets.filter((result) => result.aggConfig && result.aggConfig === aggConfig); + } + + let filters = _(aggBuckets) + .map(function (result) { + try { + return result.createFilter(); + } catch (e) { + if (!simulate) { + toastNotifications.addSuccess(e.message); + } + } + }) + .flatten() + .filter(Boolean) + .value(); + + if (!filters.length) return; + + if (event.negate) { + _.each(filters, function (filter) { + filter.meta = filter.meta || {}; + filter.meta.negate = true; + }); + } + + filters = dedupFilters($state.filters, uniqFilters(filters), { negate: true }); + + if (!simulate) { + $state.$newFilters = filters; + } + return filters; + } + }; + }; +} diff --git a/src/ui/public/filter_bar/filter_editor/generic_combo_box.tsx b/src/ui/public/filter_bar/filter_editor/generic_combo_box.tsx deleted file mode 100644 index 81b129d214e37c..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/generic_combo_box.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 } from '@elastic/eui'; -import React from 'react'; - -export interface GenericComboBoxProps { - options: T[]; - selectedOptions: T[]; - getLabel: (value: T) => string; - onChange: (values: T[]) => void; - [propName: string]: any; -} - -/** - * A generic combo box. Instead of accepting a set of options that contain a `label`, it accepts - * any type of object. It also accepts a `getLabel` function that each object will be sent through - * to get the label to be passed to the combo box. The `onChange` will trigger with the actual - * selected objects, rather than an option object. - */ -export function GenericComboBox(props: GenericComboBoxProps) { - const { options, selectedOptions, getLabel, onChange, ...otherProps } = props; - - const labels = options.map(getLabel); - const euiOptions: EuiComboBoxOptionProps[] = labels.map(label => ({ label })); - const selectedEuiOptions = selectedOptions.map(option => { - return euiOptions[options.indexOf(option)]; - }); - - const onComboBoxChange = (newOptions: EuiComboBoxOptionProps[]) => { - const newValues = newOptions.map(({ label }) => { - return options[labels.indexOf(label)]; - }); - onChange(newValues); - }; - - return ( - - ); -} diff --git a/src/ui/public/filter_bar/filter_editor/index.tsx b/src/ui/public/filter_bar/filter_editor/index.tsx deleted file mode 100644 index de301432b1b67e..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/index.tsx +++ /dev/null @@ -1,471 +0,0 @@ -/* - * 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 { - EuiButton, - EuiButtonEmpty, - // @ts-ignore - EuiCodeEditor, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiPopoverTitle, - EuiSpacer, - EuiSwitch, -} from '@elastic/eui'; -import { FieldFilter, Filter } from '@kbn/es-query'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { Component } from 'react'; -import { Field, IndexPattern } from 'ui/index_patterns'; -import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; -import { - buildCustomFilter, - buildFilter, - getFieldFromFilter, - getFilterableFields, - getFilterParams, - getIndexPatternFromFilter, - getOperatorFromFilter, - getOperatorOptions, - getQueryDslFromFilter, - isFilterValid, -} from './lib/filter_editor_utils'; -import { Operator } from './lib/filter_operators'; -import { PhraseValueInput } from './phrase_value_input'; -import { PhrasesValuesInput } from './phrases_values_input'; -import { RangeValueInput } from './range_value_input'; - -interface Props { - filter: Filter; - indexPatterns: IndexPattern[]; - onSubmit: (filter: Filter) => void; - onCancel: () => void; - intl: InjectedIntl; -} - -interface State { - selectedIndexPattern?: IndexPattern; - selectedField?: Field; - selectedOperator?: Operator; - params: any; - useCustomLabel: boolean; - customLabel: string | null; - queryDsl: string; - isCustomEditorOpen: boolean; -} - -class FilterEditorUI extends Component { - public constructor(props: Props) { - super(props); - this.state = { - selectedIndexPattern: this.getIndexPatternFromFilter(), - selectedField: this.getFieldFromFilter(), - selectedOperator: this.getSelectedOperator(), - 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 ( -
- - - - - - - - {this.state.isCustomEditorOpen ? ( - - ) : ( - - )} - - - - - -
- - {this.renderIndexPatternInput()} - - {this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()} - - - - - - {this.state.useCustomLabel && ( -
- - - - -
- )} - - - - - - - - - - - - - - - - -
-
-
- ); - } - - private renderIndexPatternInput() { - if (this.props.indexPatterns.length <= 1) { - return ''; - } - const { selectedIndexPattern } = this.state; - return ( - - - - indexPattern.title} - onChange={this.onIndexPatternChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - /> - - - - ); - } - - private renderRegularEditor() { - return ( -
- - {this.renderFieldInput()} - {this.renderOperatorInput()} - - -
{this.renderParamsEditor()}
-
- ); - } - - private renderFieldInput() { - const { selectedIndexPattern, selectedField } = this.state; - const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : []; - return ( - - field.name} - onChange={this.onFieldChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterFieldSuggestionList" - /> - - ); - } - - private renderOperatorInput() { - const { selectedField, selectedOperator } = this.state; - const operators = selectedField ? getOperatorOptions(selectedField) : []; - return ( - - message} - onChange={this.onOperatorChange} - singleSelection={{ asPlainText: true }} - isClearable={false} - data-test-subj="filterOperatorList" - /> - - ); - } - - private renderCustomEditor() { - return ( - - - - ); - } - - private renderParamsEditor() { - const indexPattern = this.state.selectedIndexPattern; - if (!indexPattern || !this.state.selectedOperator) { - return ''; - } - - switch (this.state.selectedOperator.type) { - case 'exists': - return ''; - case 'phrase': - return ( - - ); - case 'phrases': - return ( - - ); - case 'range': - 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 getIndexPatternFromFilter() { - return getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); - } - - private getFieldFromFilter() { - const indexPattern = this.getIndexPatternFromFilter(); - return indexPattern && getFieldFromFilter(this.props.filter as FieldFilter, indexPattern); - } - - private getSelectedOperator() { - return getOperatorFromFilter(this.props.filter); - } - - private isFilterValid() { - const { - isCustomEditorOpen, - queryDsl, - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - } = this.state; - - if (isCustomEditorOpen) { - try { - return Boolean(JSON.parse(queryDsl)); - } catch (e) { - return false; - } - } - - return isFilterValid(indexPattern, field, operator, params); - } - - private onIndexPatternChange = ([selectedIndexPattern]: IndexPattern[]) => { - const selectedField = undefined; - const selectedOperator = undefined; - const params = undefined; - this.setState({ selectedIndexPattern, selectedField, selectedOperator, params }); - }; - - private onFieldChange = ([selectedField]: Field[]) => { - 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) => { - const useCustomLabel = event.target.checked; - const customLabel = event.target.checked ? '' : null; - this.setState({ useCustomLabel, customLabel }); - }; - - private onCustomLabelChange = (event: React.ChangeEvent) => { - 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 { - selectedIndexPattern: indexPattern, - selectedField: field, - selectedOperator: operator, - params, - useCustomLabel, - customLabel, - isCustomEditorOpen, - queryDsl, - } = this.state; - - const { store } = this.props.filter.$state; - const alias = useCustomLabel ? customLabel : null; - - if (isCustomEditorOpen) { - const { index, disabled, negate } = this.props.filter.meta; - const newIndex = index || this.props.indexPatterns[0].id; - const body = JSON.parse(queryDsl); - const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, store); - this.props.onSubmit(filter); - } else if (indexPattern && field && operator) { - const filter = buildFilter(indexPattern, field, operator, params, alias, store); - this.props.onSubmit(filter); - } - }; -} - -function IndexPatternComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function FieldComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -function OperatorComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -export const FilterEditor = injectI18n(FilterEditorUI); diff --git a/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts deleted file mode 100644 index 859f2fcc1535a8..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -/* - * 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 { FilterStateStore, toggleFilterNegated } from '@kbn/es-query'; -import { mockFields, mockIndexPattern } from 'ui/index_patterns/fixtures'; -import { - buildFilter, - getFieldFromFilter, - getFilterableFields, - getFilterParams, - getIndexPatternFromFilter, - getOperatorFromFilter, - getOperatorOptions, - getQueryDslFromFilter, - isFilterValid, -} from './filter_editor_utils'; -import { - doesNotExistOperator, - existsOperator, - isBetweenOperator, - isOneOfOperator, - isOperator, -} from './filter_operators'; -import { existsFilter } from './fixtures/exists_filter'; -import { phraseFilter } from './fixtures/phrase_filter'; -import { phrasesFilter } from './fixtures/phrases_filter'; -import { rangeFilter } from './fixtures/range_filter'; - -describe('Filter editor utils', () => { - describe('getQueryDslFromFilter', () => { - it('should return query DSL without meta and $state', () => { - const queryDsl = getQueryDslFromFilter(phraseFilter); - expect(queryDsl).not.toHaveProperty('meta'); - expect(queryDsl).not.toHaveProperty('$state'); - }); - }); - - describe('getIndexPatternFromFilter', () => { - it('should return the index pattern from the filter', () => { - const indexPattern = getIndexPatternFromFilter(phraseFilter, [mockIndexPattern]); - expect(indexPattern).toBe(mockIndexPattern); - }); - }); - - describe('getFieldFromFilter', () => { - it('should return the field from the filter', () => { - const field = getFieldFromFilter(phraseFilter, mockIndexPattern); - expect(field).not.toBeUndefined(); - expect(field && field.name).toBe(phraseFilter.meta.key); - }); - }); - - describe('getOperatorFromFilter', () => { - it('should return "is" for phrase filter', () => { - const operator = getOperatorFromFilter(phraseFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrase'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "is not" for phrase filter', () => { - const negatedPhraseFilter = toggleFilterNegated(phraseFilter); - const operator = getOperatorFromFilter(negatedPhraseFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrase'); - expect(operator && operator.negate).toBe(true); - }); - - it('should return "is one of" for phrases filter', () => { - const operator = getOperatorFromFilter(phrasesFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrases'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "is not one of" for negated phrases filter', () => { - const negatedPhrasesFilter = toggleFilterNegated(phrasesFilter); - const operator = getOperatorFromFilter(negatedPhrasesFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('phrases'); - expect(operator && operator.negate).toBe(true); - }); - - it('should return "is between" for range filter', () => { - const operator = getOperatorFromFilter(rangeFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('range'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "is not between" for negated range filter', () => { - const negatedRangeFilter = toggleFilterNegated(rangeFilter); - const operator = getOperatorFromFilter(negatedRangeFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('range'); - expect(operator && operator.negate).toBe(true); - }); - - it('should return "exists" for exists filter', () => { - const operator = getOperatorFromFilter(existsFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('exists'); - expect(operator && operator.negate).toBe(false); - }); - - it('should return "does not exists" for negated exists filter', () => { - const negatedExistsFilter = toggleFilterNegated(existsFilter); - const operator = getOperatorFromFilter(negatedExistsFilter); - expect(operator).not.toBeUndefined(); - expect(operator && operator.type).toBe('exists'); - expect(operator && operator.negate).toBe(true); - }); - }); - - describe('getFilterParams', () => { - it('should retrieve params from phrase filter', () => { - const params = getFilterParams(phraseFilter); - expect(params).toBe('ios'); - }); - - it('should retrieve params from phrases filter', () => { - const params = getFilterParams(phrasesFilter); - expect(params).toEqual(['win xp', 'osx']); - }); - - it('should retrieve params from range filter', () => { - const params = getFilterParams(rangeFilter); - expect(params).toEqual({ from: 0, to: 10 }); - }); - - it('should return undefined for exists filter', () => { - const params = getFilterParams(existsFilter); - expect(params).toBeUndefined(); - }); - }); - - describe('getFilterableFields', () => { - it('returns the list of fields from the given index pattern', () => { - const fieldOptions = getFilterableFields(mockIndexPattern); - expect(fieldOptions.length).toBeGreaterThan(0); - }); - - it('limits the fields to the filterable fields', () => { - const fieldOptions = getFilterableFields(mockIndexPattern); - const nonFilterableFields = fieldOptions.filter(field => !field.filterable); - expect(nonFilterableFields.length).toBe(0); - }); - }); - - describe('getOperatorOptions', () => { - it('returns range for number fields', () => { - const [field] = mockFields.filter(({ type }) => type === 'number'); - const operatorOptions = getOperatorOptions(field); - const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); - expect(rangeOperator).not.toBeUndefined(); - }); - - it('does not return range for string fields', () => { - const [field] = mockFields.filter(({ type }) => type === 'string'); - const operatorOptions = getOperatorOptions(field); - const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); - expect(rangeOperator).toBeUndefined(); - }); - }); - - describe('isFilterValid', () => { - it('should return false if index pattern is not provided', () => { - const isValid = isFilterValid(undefined, mockFields[0], isOperator, 'foo'); - expect(isValid).toBe(false); - }); - - it('should return false if field is not provided', () => { - const isValid = isFilterValid(mockIndexPattern, undefined, isOperator, 'foo'); - expect(isValid).toBe(false); - }); - - it('should return false if operator is not provided', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], undefined, 'foo'); - expect(isValid).toBe(false); - }); - - it('should return false for phrases filter without phrases', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isOneOfOperator, []); - expect(isValid).toBe(false); - }); - - it('should return true for phrases filter with phrases', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isOneOfOperator, ['foo']); - expect(isValid).toBe(true); - }); - - it('should return false for range filter without range', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isBetweenOperator, undefined); - expect(isValid).toBe(false); - }); - - it('should return true for range filter with from', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isBetweenOperator, { - from: 'foo', - }); - expect(isValid).toBe(true); - }); - - it('should return true for range filter with from/to', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], isBetweenOperator, { - from: 'foo', - too: 'goo', - }); - expect(isValid).toBe(true); - }); - - it('should return true for exists filter without params', () => { - const isValid = isFilterValid(mockIndexPattern, mockFields[0], existsOperator); - expect(isValid).toBe(true); - }); - }); - - describe('buildFilter', () => { - it('should build phrase filters', () => { - const params = 'foo'; - const alias = 'bar'; - const state = FilterStateStore.APP_STATE; - const filter = buildFilter(mockIndexPattern, mockFields[0], isOperator, params, alias, state); - expect(filter.meta.negate).toBe(isOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); - }); - - it('should build phrases filters', () => { - const params = ['foo', 'bar']; - const alias = 'bar'; - const state = FilterStateStore.APP_STATE; - const filter = buildFilter( - mockIndexPattern, - mockFields[0], - isOneOfOperator, - params, - alias, - state - ); - expect(filter.meta.type).toBe(isOneOfOperator.type); - expect(filter.meta.negate).toBe(isOneOfOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); - }); - - it('should build range filters', () => { - const params = { from: 'foo', to: 'qux' }; - const alias = 'bar'; - const state = FilterStateStore.APP_STATE; - const filter = buildFilter( - mockIndexPattern, - mockFields[0], - isBetweenOperator, - params, - alias, - state - ); - expect(filter.meta.negate).toBe(isBetweenOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); - }); - - it('should build exists filters', () => { - const params = undefined; - const alias = 'bar'; - const state = FilterStateStore.APP_STATE; - const filter = buildFilter( - mockIndexPattern, - mockFields[0], - existsOperator, - params, - alias, - state - ); - expect(filter.meta.negate).toBe(existsOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); - }); - - it('should negate based on operator', () => { - const params = undefined; - const alias = 'bar'; - const state = FilterStateStore.APP_STATE; - const filter = buildFilter( - mockIndexPattern, - mockFields[0], - doesNotExistOperator, - params, - alias, - state - ); - expect(filter.meta.negate).toBe(doesNotExistOperator.negate); - expect(filter.meta.alias).toBe(alias); - expect(filter.$state.store).toBe(state); - }); - }); -}); diff --git a/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.ts deleted file mode 100644 index b1b456e482ac95..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 dateMath from '@elastic/datemath'; -import { - buildExistsFilter, - buildPhraseFilter, - buildPhrasesFilter, - buildRangeFilter, - FieldFilter, - Filter, - FilterMeta, - FilterStateStore, - PhraseFilter, - PhrasesFilter, - RangeFilter, -} from '@kbn/es-query'; -import { omit } from 'lodash'; -import { Field, IndexPattern } from 'ui/index_patterns'; -import { isFilterable } from 'ui/index_patterns/static_utils'; -import Ipv4Address from 'ui/utils/ipv4_address'; -import { FILTER_OPERATORS, Operator } from './filter_operators'; - -export function getIndexPatternFromFilter( - filter: Filter, - 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 => field.name === filter.meta.key); -} - -export function getOperatorFromFilter(filter: Filter) { - return FILTER_OPERATORS.find(operator => { - return filter.meta.type === operator.type && filter.meta.negate === operator.negate; - }); -} - -export function getQueryDslFromFilter(filter: Filter) { - return omit(filter, ['$state', 'meta']); -} - -export function getFilterableFields(indexPattern: IndexPattern) { - return indexPattern.fields.filter(isFilterable); -} - -export function getOperatorOptions(field: Field) { - return FILTER_OPERATORS.filter(operator => { - return !operator.fieldTypes || operator.fieldTypes.includes(field.type); - }); -} - -export function getFilterParams(filter: Filter) { - switch (filter.meta.type) { - case 'phrase': - return (filter as PhraseFilter).meta.params.query; - case 'phrases': - return (filter as PhrasesFilter).meta.params; - case 'range': - return { - from: (filter as RangeFilter).meta.params.gte, - to: (filter as RangeFilter).meta.params.lt, - }; - } -} - -export function validateParams(params: any, type: string) { - switch (type) { - case 'date': - const moment = typeof params === 'string' ? dateMath.parse(params) : null; - return Boolean(typeof params === 'string' && moment && moment.isValid()); - case 'ip': - try { - return Boolean(new Ipv4Address(params)); - } catch (e) { - return false; - } - default: - return true; - } -} - -export function isFilterValid( - indexPattern?: IndexPattern, - field?: Field, - operator?: Operator, - params?: any -) { - if (!indexPattern || !field || !operator) { - return false; - } - switch (operator.type) { - case 'phrase': - return validateParams(params, field.type); - case 'phrases': - if (!Array.isArray(params) || !params.length) { - return false; - } - return params.every(phrase => validateParams(phrase, field.type)); - case 'range': - if (typeof params !== 'object') { - return false; - } - return validateParams(params.from, field.type) || validateParams(params.to, field.type); - case 'exists': - return true; - default: - throw new Error(`Unknown operator type: ${operator.type}`); - } -} - -export function buildFilter( - indexPattern: IndexPattern, - field: Field, - operator: Operator, - params: any, - alias: string | null, - store: FilterStateStore -): Filter { - const filter = buildBaseFilter(indexPattern, field, operator, params); - filter.meta.alias = alias; - filter.meta.negate = operator.negate; - filter.$state = { store }; - return filter; -} - -function buildBaseFilter( - indexPattern: IndexPattern, - field: Field, - operator: Operator, - params: any -): Filter { - switch (operator.type) { - case 'phrase': - return buildPhraseFilter(field, params, indexPattern); - case 'phrases': - return buildPhrasesFilter(field, params, indexPattern); - case 'range': - const newParams = { gte: params.from, lt: params.to }; - return buildRangeFilter(field, newParams, indexPattern); - case 'exists': - return buildExistsFilter(field, indexPattern); - default: - throw new Error(`Unknown operator type: ${operator.type}`); - } -} - -export function buildCustomFilter( - index: string, - queryDsl: any, - disabled: boolean, - negate: boolean, - alias: string | null, - store: FilterStateStore -): Filter { - const meta: FilterMeta = { index, type: 'custom', disabled, negate, alias }; - const filter: Filter = { ...queryDsl, meta }; - filter.$state = { store }; - return filter; -} diff --git a/src/ui/public/filter_bar/filter_editor/lib/filter_operators.ts b/src/ui/public/filter_bar/filter_editor/lib/filter_operators.ts deleted file mode 100644 index b97f4a73901b62..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/lib/filter_operators.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -export interface Operator { - message: string; - type: string; - negate: boolean; - fieldTypes?: string[]; -} - -export const isOperator = { - message: i18n.translate('common.ui.filterEditor.isOperatorOptionLabel', { - defaultMessage: 'is', - }), - type: 'phrase', - negate: false, -}; - -export const isNotOperator = { - message: i18n.translate('common.ui.filterEditor.isNotOperatorOptionLabel', { - defaultMessage: 'is not', - }), - type: 'phrase', - negate: true, -}; - -export const isOneOfOperator = { - message: i18n.translate('common.ui.filterEditor.isOneOfOperatorOptionLabel', { - defaultMessage: 'is one of', - }), - type: 'phrases', - negate: false, - fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], -}; - -export const isNotOneOfOperator = { - message: i18n.translate('common.ui.filterEditor.isNotOneOfOperatorOptionLabel', { - defaultMessage: 'is not one of', - }), - type: 'phrases', - negate: true, - fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'], -}; - -export const isBetweenOperator = { - message: i18n.translate('common.ui.filterEditor.isBetweenOperatorOptionLabel', { - defaultMessage: 'is between', - }), - type: 'range', - negate: false, - fieldTypes: ['number', 'date', 'ip'], -}; - -export const isNotBetweenOperator = { - message: i18n.translate('common.ui.filterEditor.isNotBetweenOperatorOptionLabel', { - defaultMessage: 'is not between', - }), - type: 'range', - negate: true, - fieldTypes: ['number', 'date', 'ip'], -}; - -export const existsOperator = { - message: i18n.translate('common.ui.filterEditor.existsOperatorOptionLabel', { - defaultMessage: 'exists', - }), - type: 'exists', - negate: false, -}; - -export const doesNotExistOperator = { - message: i18n.translate('common.ui.filterEditor.doesNotExistOperatorOptionLabel', { - defaultMessage: 'does not exist', - }), - type: 'exists', - negate: true, -}; - -export const FILTER_OPERATORS: Operator[] = [ - isOperator, - isNotOperator, - isOneOfOperator, - isNotOneOfOperator, - isBetweenOperator, - isNotBetweenOperator, - existsOperator, - doesNotExistOperator, -]; diff --git a/src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx b/src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx deleted file mode 100644 index 75073def34281e..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 { Field, IndexPattern } from 'ui/index_patterns'; -import { getSuggestions } from 'ui/value_suggestions'; -const config = chrome.getUiSettingsClient(); - -export interface PhraseSuggestorProps { - indexPattern: IndexPattern; - field?: Field; -} - -export interface PhraseSuggestorState { - suggestions: string[]; - isLoading: boolean; -} - -/** - * Since both "phrase" and "phrases" filter inputs suggest values (if enabled and the field is - * aggregatable), we pull out the common logic for requesting suggestions into this component - * which both of them extend. - */ -export 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/filter_editor/phrase_value_input.tsx b/src/ui/public/filter_bar/filter_editor/phrase_value_input.tsx deleted file mode 100644 index 06b826d681d0cd..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/phrase_value_input.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 { EuiFormRow } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { uniq } from 'lodash'; -import React from 'react'; -import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; -import { PhraseSuggestor, PhraseSuggestorProps } from './phrase_suggestor'; -import { ValueInputType } from './value_input_type'; - -interface Props extends PhraseSuggestorProps { - value?: string; - onChange: (value: string | number | boolean) => void; - intl: InjectedIntl; -} - -class PhraseValueInputUI extends PhraseSuggestor { - public render() { - return ( - - {this.isSuggestingValues() ? ( - this.renderWithSuggestions() - ) : ( - - )} - - ); - } - - private renderWithSuggestions() { - const { suggestions } = this.state; - const { value, intl, onChange } = this.props; - const options = value ? uniq([value, ...suggestions]) : suggestions; - return ( - option} - selectedOptions={value ? [value] : []} - onChange={([newValue = '']) => onChange(newValue)} - onSearchChange={this.onSearchChange} - singleSelection={{ asPlainText: true }} - onCreateOption={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phraseParamsComboxBox" - /> - ); - } -} - -function StringComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -export const PhraseValueInput = injectI18n(PhraseValueInputUI); diff --git a/src/ui/public/filter_bar/filter_editor/phrases_values_input.tsx b/src/ui/public/filter_bar/filter_editor/phrases_values_input.tsx deleted file mode 100644 index bef1433223e74e..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/phrases_values_input.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 { EuiFormRow } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { uniq } from 'lodash'; -import React from 'react'; -import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; -import { PhraseSuggestor, PhraseSuggestorProps } from './phrase_suggestor'; - -interface Props extends PhraseSuggestorProps { - values?: string[]; - onChange: (values: string[]) => void; - intl: InjectedIntl; -} - -class PhrasesValuesInputUI extends PhraseSuggestor { - public render() { - const { suggestions } = this.state; - const { values, intl, onChange } = this.props; - const options = values ? uniq([...values, ...suggestions]) : suggestions; - return ( - - option} - selectedOptions={values || []} - onCreateOption={(option: string) => onChange([...(values || []), option])} - onChange={onChange} - isClearable={false} - data-test-subj="filterParamsComboBox phrasesParamsComboxBox" - /> - - ); - } -} - -function StringComboBox(props: GenericComboBoxProps) { - return GenericComboBox(props); -} - -export const PhrasesValuesInput = injectI18n(PhrasesValuesInputUI); diff --git a/src/ui/public/filter_bar/filter_editor/range_value_input.tsx b/src/ui/public/filter_bar/filter_editor/range_value_input.tsx deleted file mode 100644 index 7343c5722f226e..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/range_value_input.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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, EuiFormRow, EuiIcon, EuiLink } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import { Component } from 'react'; -import React from 'react'; -import { getDocLink } from 'ui/documentation_links'; -import { Field } from 'ui/index_patterns'; -import { ValueInputType } from './value_input_type'; - -interface RangeParams { - from: number | string; - to: number | string; -} - -type RangeParamsPartial = Partial; - -interface Props { - field?: Field; - value?: RangeParams; - onChange: (params: RangeParamsPartial) => void; - intl: InjectedIntl; -} - -class RangeValueInputUI extends Component { - public constructor(props: Props) { - super(props); - } - - public render() { - const type = this.props.field ? this.props.field.type : 'string'; - - return ( -
- - - - - - - - - - - - - {type === 'date' ? ( - - {' '} - - - ) : ( - '' - )} -
- ); - } - - private onFromChange = (value: string | number | boolean) => { - if (typeof value !== 'string' && typeof value !== 'number') { - throw new Error('Range params must be a string or number'); - } - this.props.onChange({ from: value, to: get(this, 'props.value.to') }); - }; - - private onToChange = (value: string | number | boolean) => { - if (typeof value !== 'string' && typeof value !== 'number') { - throw new Error('Range params must be a string or number'); - } - this.props.onChange({ from: get(this, 'props.value.from'), to: value }); - }; -} - -export const RangeValueInput = injectI18n(RangeValueInputUI); diff --git a/src/ui/public/filter_bar/filter_editor/value_input_type.tsx b/src/ui/public/filter_bar/filter_editor/value_input_type.tsx deleted file mode 100644 index 0a573c88eae703..00000000000000 --- a/src/ui/public/filter_bar/filter_editor/value_input_type.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { isEmpty } from 'lodash'; -import React, { Component } from 'react'; -import { validateParams } from './lib/filter_editor_utils'; - -interface Props { - value?: string | number; - type: string; - onChange: (value: string | number | boolean) => void; - placeholder: string; - intl: InjectedIntl; -} - -class ValueInputTypeUI extends Component { - public render() { - const value = this.props.value; - 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) => { - const params = event.target.value; - this.props.onChange(params); - }; -} - -export const ValueInputType = injectI18n(ValueInputTypeUI); diff --git a/src/ui/public/filter_bar/filter_item.tsx b/src/ui/public/filter_bar/filter_item.tsx deleted file mode 100644 index 82ed8c692d5497..00000000000000 --- a/src/ui/public/filter_bar/filter_item.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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 { EuiContextMenu, EuiPopover } from '@elastic/eui'; -import { - Filter, - isFilterPinned, - toggleFilterDisabled, - toggleFilterNegated, - toggleFilterPinned, -} from '@kbn/es-query'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import classNames from 'classnames'; -import React, { Component } from 'react'; -import { IndexPattern } from 'ui/index_patterns'; -import { FilterEditor } from './filter_editor'; -import { FilterView } from './filter_view'; - -interface Props { - id: string; - filter: Filter; - indexPatterns: IndexPattern[]; - className?: string; - onUpdate: (filter: Filter) => void; - onRemove: () => void; - intl: InjectedIntl; -} - -interface State { - isPopoverOpen: boolean; -} - -class FilterItemUI extends Component { - public state = { - isPopoverOpen: false, - }; - - public render() { - const { filter, id } = this.props; - const { negate, disabled } = filter.meta; - - const classes = classNames( - 'globalFilterItem', - { - 'globalFilterItem-isDisabled': disabled, - 'globalFilterItem-isPinned': isFilterPinned(filter), - 'globalFilterItem-isExcluded': negate, - }, - this.props.className - ); - - const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; - const dataTestSubjValue = filter.meta.value ? `filter-value-${filter.meta.value}` : ''; - const dataTestSubjDisabled = `filter-${ - this.props.filter.meta.disabled ? 'disabled' : 'enabled' - }`; - - const badge = ( - this.props.onRemove()} - onClick={this.togglePopover} - data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue}`} - /> - ); - - const panelTree = [ - { - id: 0, - items: [ - { - name: isFilterPinned(filter) - ? this.props.intl.formatMessage({ - id: 'common.ui.filterBar.unpinFilterButtonLabel', - defaultMessage: 'Unpin', - }) - : this.props.intl.formatMessage({ - id: 'common.ui.filterBar.pinFilterButtonLabel', - defaultMessage: 'Pin across all apps', - }), - icon: 'pin', - onClick: () => { - this.closePopover(); - this.onTogglePinned(); - }, - 'data-test-subj': 'pinFilter', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.filterBar.editFilterButtonLabel', - defaultMessage: 'Edit filter', - }), - icon: 'pencil', - panel: 1, - 'data-test-subj': 'editFilter', - }, - { - name: negate - ? this.props.intl.formatMessage({ - id: 'common.ui.filterBar.includeFilterButtonLabel', - defaultMessage: 'Include results', - }) - : this.props.intl.formatMessage({ - id: 'common.ui.filterBar.excludeFilterButtonLabel', - defaultMessage: 'Exclude results', - }), - icon: negate ? 'plusInCircle' : 'minusInCircle', - onClick: () => { - this.closePopover(); - this.onToggleNegated(); - }, - 'data-test-subj': 'negateFilter', - }, - { - name: disabled - ? this.props.intl.formatMessage({ - id: 'common.ui.filterBar.enableFilterButtonLabel', - defaultMessage: 'Re-enable', - }) - : this.props.intl.formatMessage({ - id: 'common.ui.filterBar.disableFilterButtonLabel', - defaultMessage: 'Temporarily disable', - }), - icon: `${disabled ? 'eye' : 'eyeClosed'}`, - onClick: () => { - this.closePopover(); - this.onToggleDisabled(); - }, - 'data-test-subj': 'disableFilter', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.filterBar.deleteFilterButtonLabel', - defaultMessage: 'Delete', - }), - icon: 'trash', - onClick: () => { - this.closePopover(); - this.props.onRemove(); - }, - 'data-test-subj': 'deleteFilter', - }, - ], - }, - { - id: 1, - width: 400, - content: ( -
- -
- ), - }, - ]; - - return ( - - - - ); - } - - private closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; - - private togglePopover = () => { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - }; - - private onSubmit = (filter: Filter) => { - 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); - }; -} - -export const FilterItem = injectI18n(FilterItemUI); diff --git a/src/ui/public/filter_bar/filter_pill/filter_pill.html b/src/ui/public/filter_bar/filter_pill/filter_pill.html new file mode 100644 index 00000000000000..f79d42665e8fc0 --- /dev/null +++ b/src/ui/public/filter_bar/filter_pill/filter_pill.html @@ -0,0 +1,97 @@ +
+ +
+ + NOT + {{ pill.filter.meta.alias }} + {{ pill.filter.meta.key }}: + "{{ pill.filter.meta.value }}" +
+ +
+ + + + + + + + + + +
+
diff --git a/src/ui/public/filter_bar/filter_pill/filter_pill.js b/src/ui/public/filter_bar/filter_pill/filter_pill.js new file mode 100644 index 00000000000000..03b19427af9300 --- /dev/null +++ b/src/ui/public/filter_bar/filter_pill/filter_pill.js @@ -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 _ from 'lodash'; +import template from './filter_pill.html'; +import { uiModules } from '../../modules'; + +const module = uiModules.get('kibana'); + +module.directive('filterPill', function () { + return { + template, + restrict: 'E', + scope: { + filter: '=', + onToggleFilter: '=', + onPinFilter: '=', + onInvertFilter: '=', + onDeleteFilter: '=', + onEditFilter: '=', + }, + bindToController: true, + controllerAs: 'pill', + controller: function filterPillController() { + + this.activateActions = () => { + this.areActionsActivated = true; + }; + + this.deactivateActions = () => { + this.areActionsActivated = false; + }; + + this.isControlledByPanel = () => { + return _.has(this.filter, 'meta.controlledBy'); + }; + + } + }; +}); + diff --git a/src/ui/public/search_bar/components/index.tsx b/src/ui/public/filter_bar/filter_pill/index.js similarity index 95% rename from src/ui/public/search_bar/components/index.tsx rename to src/ui/public/filter_bar/filter_pill/index.js index 131c6059934a47..3db610588eb754 100644 --- a/src/ui/public/search_bar/components/index.tsx +++ b/src/ui/public/filter_bar/filter_pill/index.js @@ -17,4 +17,4 @@ * under the License. */ -export { SearchBar } from './search_bar'; +import './filter_pill'; diff --git a/src/ui/public/filter_bar/filter_view/index.tsx b/src/ui/public/filter_bar/filter_view/index.tsx deleted file mode 100644 index 2421e7dcfab97e..00000000000000 --- a/src/ui/public/filter_bar/filter_view/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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 { EuiBadge } from '@elastic/eui'; -import { Filter, isFilterPinned } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; -import React, { SFC } from 'react'; -import { existsOperator, isOneOfOperator } from 'ui/filter_bar/filter_editor/lib/filter_operators'; - -interface Props { - filter: Filter; - [propName: string]: any; -} - -export const FilterView: SFC = ({ filter, ...rest }: Props) => { - let title = `Filter: ${getFilterDisplayText(filter)}. ${i18n.translate( - 'common.ui.filterBar.moreFilterActionsMessage', - { - defaultMessage: 'Select for more filter actions.', - } - )}`; - - if (isFilterPinned(filter)) { - title = `${i18n.translate('common.ui.filterBar.pinnedFilterPrefix', { - defaultMessage: 'Pinned', - })} ${title}`; - } - if (filter.meta.disabled) { - title = `${i18n.translate('common.ui.filterBar.disabledFilterPrefix', { - defaultMessage: 'Disabled', - })} ${title}`; - } - - return ( - - {getFilterDisplayText(filter)} - - ); -}; - -export function getFilterDisplayText(filter: Filter) { - if (filter.meta.alias !== null) { - return filter.meta.alias; - } - - const prefix = filter.meta.negate - ? ` ${i18n.translate('common.ui.filterBar.negatedFilterPrefix', { - defaultMessage: 'NOT ', - })}` - : ''; - - switch (filter.meta.type) { - case 'exists': - return `${prefix}${filter.meta.key} ${existsOperator.message}`; - case 'geo_bounding_box': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - case 'geo_polygon': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - case 'phrase': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - case 'phrases': - return `${prefix}${filter.meta.key} ${isOneOfOperator.message} ${filter.meta.value}`; - case 'query_string': - return `${prefix}${filter.meta.value}`; - case 'range': - return `${prefix}${filter.meta.key}: ${filter.meta.value}`; - default: - return `${prefix}${JSON.stringify(filter.query)}`; - } -} diff --git a/src/ui/public/search_bar/index.tsx b/src/ui/public/filter_bar/index.js similarity index 86% rename from src/ui/public/search_bar/index.tsx rename to src/ui/public/filter_bar/index.js index 2469f62781f972..082a17b501ed97 100644 --- a/src/ui/public/search_bar/index.tsx +++ b/src/ui/public/filter_bar/index.js @@ -17,6 +17,6 @@ * under the License. */ -import './directive'; +import './filter_bar'; // directive -export { SearchBar } from './components'; +export { disableFilter, enableFilter, toggleFilterDisabled } from './lib/disable_filter'; diff --git a/src/ui/public/filter_bar/index.ts b/src/ui/public/filter_bar/index.ts deleted file mode 100644 index cdf49a72e9554e..00000000000000 --- a/src/ui/public/filter_bar/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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'; diff --git a/src/ui/public/filter_bar/lib/__tests__/disable_filter.js b/src/ui/public/filter_bar/lib/__tests__/disable_filter.js new file mode 100644 index 00000000000000..2541ec6cacae08 --- /dev/null +++ b/src/ui/public/filter_bar/lib/__tests__/disable_filter.js @@ -0,0 +1,140 @@ +/* + * 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 expect from 'expect.js'; + +import { + disableFilter, + enableFilter, + toggleFilterDisabled, +} from '../disable_filter'; + + +describe('function disableFilter', function () { + it('should disable a filter that is explicitly enabled', function () { + const enabledFilter = { + meta: { + disabled: false, + }, + match_all: {}, + }; + + expect(disableFilter(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should disable a filter that is implicitly enabled', function () { + const enabledFilter = { + match_all: {}, + }; + + expect(disableFilter(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should preserve other properties', function () { + const enabledFilterWithProperties = { + meta: { + meta_property: 'META_PROPERTY', + }, + match_all: {}, + }; + + const disabledFilter = disableFilter(enabledFilterWithProperties); + expect(disabledFilter).to.have.property('match_all', enabledFilterWithProperties.match_all); + expect(disabledFilter.meta).to.have.property('meta_property', enabledFilterWithProperties.meta_property); + }); +}); + +describe('function enableFilter', function () { + it('should enable a filter that is disabled', function () { + const disabledFilter = { + meta: { + disabled: true, + }, + match_all: {}, + }; + + expect(enableFilter(disabledFilter).meta).to.have.property('disabled', false); + }); + + it('should explicitly enable a filter that is implicitly enabled', function () { + const enabledFilter = { + match_all: {}, + }; + + expect(enableFilter(enabledFilter).meta).to.have.property('disabled', false); + }); + + it('should preserve other properties', function () { + const enabledFilterWithProperties = { + meta: { + meta_property: 'META_PROPERTY', + }, + match_all: {}, + }; + + const enabledFilter = enableFilter(enabledFilterWithProperties); + expect(enabledFilter).to.have.property('match_all', enabledFilterWithProperties.match_all); + expect(enabledFilter.meta).to.have.property('meta_property', enabledFilterWithProperties.meta_property); + }); +}); + +describe('function toggleFilterDisabled', function () { + it('should enable a filter that is disabled', function () { + const disabledFilter = { + meta: { + disabled: true, + }, + match_all: {}, + }; + + expect(toggleFilterDisabled(disabledFilter).meta).to.have.property('disabled', false); + }); + + it('should disable a filter that is explicitly enabled', function () { + const enabledFilter = { + meta: { + disabled: false, + }, + match_all: {}, + }; + + expect(toggleFilterDisabled(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should disable a filter that is implicitly enabled', function () { + const enabledFilter = { + match_all: {}, + }; + + expect(toggleFilterDisabled(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should preserve other properties', function () { + const enabledFilterWithProperties = { + meta: { + meta_property: 'META_PROPERTY', + }, + match_all: {}, + }; + + const disabledFilter = toggleFilterDisabled(enabledFilterWithProperties); + expect(disabledFilter).to.have.property('match_all', enabledFilterWithProperties.match_all); + expect(disabledFilter.meta).to.have.property('meta_property', enabledFilterWithProperties.meta_property); + }); +}); diff --git a/packages/kbn-es-query/src/filters/lib/exists_filter.ts b/src/ui/public/filter_bar/lib/__tests__/filter_applied_and_unwrap.js similarity index 56% rename from packages/kbn-es-query/src/filters/lib/exists_filter.ts rename to src/ui/public/filter_bar/lib/__tests__/filter_applied_and_unwrap.js index 356d039f4d19bd..819035452837e3 100644 --- a/packages/kbn-es-query/src/filters/lib/exists_filter.ts +++ b/src/ui/public/filter_bar/lib/__tests__/filter_applied_and_unwrap.js @@ -17,10 +17,22 @@ * under the License. */ -import { Filter, FilterMeta } from './meta_filter'; +import expect from 'expect.js'; +import { filterAppliedAndUnwrap } from '../filter_applied_and_unwrap'; -export type ExistsFilterMeta = FilterMeta; +describe('Filter Bar Directive', function () { + describe('filterAppliedAndUnwrap()', function () { -export type ExistsFilter = Filter & { - meta: ExistsFilterMeta; -}; + const filters = [ + { meta: { apply: true }, exists: { field: '_type' } }, + { meta: { apply: false }, query: { query_string: { query: 'foo:bar' } } } + ]; + + it('should filter the applied and unwrap the filter', function () { + const results = filterAppliedAndUnwrap(filters); + expect(results).to.have.length(1); + expect(results[0]).to.eql(filters[0]); + }); + + }); +}); diff --git a/src/ui/public/filter_bar/lib/__tests__/filter_out_time_based_filter.js b/src/ui/public/filter_bar/lib/__tests__/filter_out_time_based_filter.js new file mode 100644 index 00000000000000..009683e3c07f44 --- /dev/null +++ b/src/ui/public/filter_bar/lib/__tests__/filter_out_time_based_filter.js @@ -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 expect from 'expect.js'; +import ngMock from 'ng_mock'; +import { FilterBarLibFilterOutTimeBasedFilterProvider } from '../filter_out_time_based_filter'; + +describe('Filter Bar Directive', function () { + describe('filterOutTimeBasedFilter()', function () { + + let filterOutTimeBasedFilter; + let $rootScope; + + beforeEach(ngMock.module( + 'kibana', + 'kibana/courier', + function ($provide) { + $provide.service('indexPatterns', require('fixtures/mock_index_patterns')); + } + )); + + beforeEach(ngMock.inject(function (Private, _$rootScope_) { + filterOutTimeBasedFilter = Private(FilterBarLibFilterOutTimeBasedFilterProvider); + $rootScope = _$rootScope_; + })); + + it('should return the matching filter for the default time field', function (done) { + const filters = [ + { meta: { index: 'logstash-*' }, query: { match: { _type: { query: 'apache', type: 'phrase' } } } }, + { meta: { index: 'logstash-*' }, range: { 'time': { gt: 1388559600000, lt: 1388646000000 } } } + ]; + filterOutTimeBasedFilter(filters).then(function (results) { + expect(results).to.have.length(1); + expect(results).to.not.contain(filters[1]); + done(); + }); + $rootScope.$apply(); + }); + + }); +}); diff --git a/src/ui/public/filter_bar/lib/disable_filter.js b/src/ui/public/filter_bar/lib/disable_filter.js new file mode 100644 index 00000000000000..815bc4eb2a83ac --- /dev/null +++ b/src/ui/public/filter_bar/lib/disable_filter.js @@ -0,0 +1,44 @@ +/* + * 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 function disableFilter(filter) { + return setFilterDisabled(filter, true); +} + +export function enableFilter(filter) { + return setFilterDisabled(filter, false); +} + +export function toggleFilterDisabled(filter) { + const { meta: { disabled = false } = {} } = filter; + + return setFilterDisabled(filter, !disabled); +} + +function setFilterDisabled(filter, disabled) { + const { meta = {} } = filter; + + return { + ...filter, + meta: { + ...meta, + disabled, + } + }; +} diff --git a/src/ui/public/apply_filters/index.ts b/src/ui/public/filter_bar/lib/filter_applied_and_unwrap.js similarity index 87% rename from src/ui/public/apply_filters/index.ts rename to src/ui/public/filter_bar/lib/filter_applied_and_unwrap.js index 4346316e623740..843ff6daf10494 100644 --- a/src/ui/public/apply_filters/index.ts +++ b/src/ui/public/filter_bar/lib/filter_applied_and_unwrap.js @@ -17,6 +17,9 @@ * under the License. */ -import './directive'; +import _ from 'lodash'; + +export function filterAppliedAndUnwrap(filters) { + return _.filter(filters, 'meta.apply'); +} -export { ApplyFiltersPopover } from './apply_filters_popover'; diff --git a/packages/kbn-es-query/src/filters/lib/range_filter.ts b/src/ui/public/filter_bar/lib/filter_out_time_based_filter.js similarity index 64% rename from packages/kbn-es-query/src/filters/lib/range_filter.ts rename to src/ui/public/filter_bar/lib/filter_out_time_based_filter.js index 214652fb3f3327..0aa628e6ad069d 100644 --- a/packages/kbn-es-query/src/filters/lib/range_filter.ts +++ b/src/ui/public/filter_bar/lib/filter_out_time_based_filter.js @@ -17,19 +17,17 @@ * under the License. */ -import { Filter, FilterMeta } from './meta_filter'; +import _ from 'lodash'; -export interface RangeFilterParams { - gt?: number | string; - gte?: number | string; - lte?: number | string; - lt?: number | string; -} - -export type RangeFilterMeta = FilterMeta & { - params: RangeFilterParams; -}; +export function FilterBarLibFilterOutTimeBasedFilterProvider(indexPatterns, Promise) { + return Promise.method(function (filters) { + const id = _.get(filters, '[0].meta.index'); + if (id == null) return; -export type RangeFilter = Filter & { - meta: RangeFilterMeta; -}; + return indexPatterns.get(id).then(function (indexPattern) { + return _.filter(filters, function (filter) { + return !(filter.range && filter.range[indexPattern.timeFieldName]); + }); + }); + }); +} diff --git a/src/ui/public/filter_bar/lib/map_phrase.js b/src/ui/public/filter_bar/lib/map_phrase.js index 687c844f7eec78..25018d4746e2bc 100644 --- a/src/ui/public/filter_bar/lib/map_phrase.js +++ b/src/ui/public/filter_bar/lib/map_phrase.js @@ -30,8 +30,8 @@ export function FilterBarLibMapPhraseProvider(Promise, indexPatterns) { function getParams(indexPattern) { const type = 'phrase'; const key = isScriptedPhraseFilter ? filter.meta.field : Object.keys(filter.query.match)[0]; - const query = isScriptedPhraseFilter ? filter.script.script.params.value : filter.query.match[key].query; - const params = { query }; + const params = isScriptedPhraseFilter ? filter.script.script.params : filter.query.match[key]; + const query = isScriptedPhraseFilter ? params.value : params.query; // Sometimes a filter will end up with an invalid index or field param. This could happen for a lot of reasons, // for example a user might manually edit the url or the index pattern's ID might change due to @@ -54,6 +54,6 @@ export function FilterBarLibMapPhraseProvider(Promise, indexPatterns) { } function isScriptedPhrase(filter) { - const value = _.get(filter, ['script', 'script', 'params', 'value']); - return typeof value !== 'undefined'; + const params = _.get(filter, ['script', 'script', 'params']); + return params && params.value; } diff --git a/src/ui/public/filter_bar/query_filter.js b/src/ui/public/filter_bar/query_filter.js index 5b73013973fb81..0c1e68c084535e 100644 --- a/src/ui/public/filter_bar/query_filter.js +++ b/src/ui/public/filter_bar/query_filter.js @@ -24,13 +24,10 @@ import { uniqFilters } from './lib/uniq_filters'; import { compareFilters } from './lib/compare_filters'; import { EventsProvider } from '../events'; import { FilterBarLibMapAndFlattenFiltersProvider } from './lib/map_and_flatten_filters'; -import { FilterBarLibExtractTimeFilterProvider } from './lib/extract_time_filter'; -import { changeTimeFilter } from './lib/change_time_filter'; export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, globalState, config) { const EventEmitter = Private(EventsProvider); const mapAndFlattenFilters = Private(FilterBarLibMapAndFlattenFiltersProvider); - const extractTimeFilter = Private(FilterBarLibExtractTimeFilterProvider); const queryFilter = new EventEmitter(); @@ -219,24 +216,6 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g executeOnFilters(pin); }; - queryFilter.setFilters = filters => { - return mapAndFlattenFilters(filters) - .then(mappedFilters => { - const appState = getAppState(); - const [globalFilters, appFilters] = _.partition(mappedFilters, filter => { - return filter.$state.store === 'globalState'; - }); - globalState.filters = globalFilters; - if (appState) appState.filters = appFilters; - }); - }; - - queryFilter.addFiltersAndChangeTimeFilter = async filters => { - const timeFilter = await extractTimeFilter(filters); - if (timeFilter) changeTimeFilter(timeFilter); - queryFilter.addFilters(filters.filter(filter => filter !== timeFilter)); - }; - initWatchers(); return queryFilter; @@ -289,6 +268,7 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g // ensure we don't mutate the filters passed in const globalFilters = gFilters ? _.cloneDeep(gFilters) : []; const appFilters = aFilters ? _.cloneDeep(aFilters) : []; + compareOptions = _.defaults(compareOptions || {}, { disabled: true }); // existing globalFilters should be mutated by appFilters _.each(appFilters, function (filter, i) { @@ -313,8 +293,8 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g return [ // Reverse filters after uniq again, so they are still in the order, they // were before updating them - uniqFilters(globalFilters).reverse(), - uniqFilters(appFilters).reverse() + uniqFilters(globalFilters, { disabled: true }).reverse(), + uniqFilters(appFilters, { disabled: true }).reverse() ]; } @@ -352,7 +332,8 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g // reconcile filter in global and app states const filters = mergeStateFilters(next[0], next[1]); - const [globalFilters, appFilters] = filters; + const globalFilters = filters[0]; + const appFilters = filters[1]; const appState = getAppState(); // save the state, as it may have updated diff --git a/src/ui/public/filter_editor/filter_editor.html b/src/ui/public/filter_editor/filter_editor.html new file mode 100644 index 00000000000000..0b69a1d5661d8a --- /dev/null +++ b/src/ui/public/filter_editor/filter_editor.html @@ -0,0 +1,148 @@ +
+
+
+ Add + Edit + filter +
+ + +
+ +
+ +
+ + + +
+
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ + +

+ Filters are built using the Elasticsearch Query DSL. +

+
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+
diff --git a/src/ui/public/filter_editor/filter_editor.js b/src/ui/public/filter_editor/filter_editor.js new file mode 100644 index 00000000000000..ac55a253b594fd --- /dev/null +++ b/src/ui/public/filter_editor/filter_editor.js @@ -0,0 +1,161 @@ +/* + * 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 _ from 'lodash'; +import { uiModules } from '../modules'; +import { callAfterBindingsWorkaround } from '../compat'; +import { FILTER_OPERATOR_TYPES } from './lib/filter_operators'; +import template from './filter_editor.html'; +import '../directives/documentation_href'; +import './filter_query_dsl_editor'; +import './filter_field_select'; +import './filter_operator_select'; +import './params_editor/filter_params_editor'; +import './filter_editor.less'; +import { + getQueryDslFromFilter, + getFieldFromFilter, + getOperatorFromFilter, + getParamsFromFilter, + isFilterValid, + buildFilter, + areIndexPatternsProvided, + isFilterPinned +} from './lib/filter_editor_utils'; +import * as filterBuilder from '@kbn/es-query'; +import { keyMap } from '../utils/key_map'; + +const module = uiModules.get('kibana'); +module.directive('filterEditor', function ($timeout, indexPatterns) { + return { + restrict: 'E', + template, + scope: { + indexPatterns: '=', + filter: '=', + onDelete: '&', + onCancel: '&', + onSave: '&' + }, + controllerAs: 'filterEditor', + bindToController: true, + controller: callAfterBindingsWorkaround(function ($scope, $element, config) { + const pinnedByDefault = config.get('filters:pinnedByDefault'); + + this.init = async () => { + if (!areIndexPatternsProvided(this.indexPatterns)) { + const defaultIndexPattern = await indexPatterns.getDefault(); + if (defaultIndexPattern) { + this.indexPatterns = [defaultIndexPattern]; + } + } + const { filter } = this; + this.alias = filter.meta.alias; + this.isEditingQueryDsl = false; + this.queryDsl = getQueryDslFromFilter(filter); + if (filter.meta.isNew) { + this.setFocus('field'); + } else { + getFieldFromFilter(filter, indexPatterns) + .then((field) => { + this.setField(field); + this.setOperator(getOperatorFromFilter(filter)); + this.params = getParamsFromFilter(filter); + }); + } + }; + + $scope.$watch(() => this.filter, this.init); + $scope.$watchCollection(() => this.filter.meta, this.init); + + this.setQueryDsl = (queryDsl) => { + this.queryDsl = queryDsl; + }; + + this.setField = (field) => { + this.field = field; + this.operator = null; + this.params = {}; + }; + + this.onFieldSelect = (field) => { + this.setField(field); + this.setFocus('operator'); + }; + + this.setOperator = (operator) => { + this.operator = operator; + }; + + this.onOperatorSelect = (operator) => { + this.setOperator(operator); + this.setFocus('params'); + }; + + this.setParams = (params) => { + this.params = params; + }; + + this.setFocus = (name) => { + $timeout(() => $scope.$broadcast(`focus-${name}`)); + }; + + this.toggleEditingQueryDsl = () => { + this.isEditingQueryDsl = !this.isEditingQueryDsl; + }; + + this.isQueryDslEditorVisible = () => { + const { type, isNew } = this.filter.meta; + return this.isEditingQueryDsl || (!isNew && !FILTER_OPERATOR_TYPES.includes(type)); + }; + + this.isValid = () => { + if (this.isQueryDslEditorVisible()) { + return _.isObject(this.queryDsl); + } + const { field, operator, params } = this; + return isFilterValid({ field, operator, params }); + }; + + this.save = () => { + const { filter, field, operator, params, alias } = this; + + let newFilter; + if (this.isQueryDslEditorVisible()) { + const meta = _.pick(filter.meta, ['negate', 'index']); + meta.index = meta.index || this.indexPatterns[0].id; + newFilter = Object.assign(this.queryDsl, { meta }); + } else { + const indexPattern = field.indexPattern; + newFilter = buildFilter({ indexPattern, field, operator, params, filterBuilder }); + } + newFilter.meta.disabled = filter.meta.disabled; + newFilter.meta.alias = alias; + const isPinned = isFilterPinned(filter, pinnedByDefault); + return this.onSave({ filter, newFilter, isPinned }); + }; + + $element.on('keydown', (event) => { + if (keyMap[event.keyCode] === 'escape') { + $timeout(() => this.onCancel()); + } + }); + }) + }; +}); diff --git a/src/ui/public/filter_editor/filter_editor.less b/src/ui/public/filter_editor/filter_editor.less new file mode 100644 index 00000000000000..b45c0030b37d39 --- /dev/null +++ b/src/ui/public/filter_editor/filter_editor.less @@ -0,0 +1,36 @@ +.filterEditor { + position: absolute; + width: 600px; + z-index: 101; +} + +.filterEditor__labelBar { + display: flex; + align-items: center; + justify-content: space-between; +} + +.filterEditor__wideField { + min-width: 0; +} + +.filterEditorParamsInput { + min-width: 100px; +} + +.uiSelectChoices--autoWidth { + width: auto !important; + min-width: 100% !important; +} + +.uiSelectMatch--restrictToParent .ui-select-match-item { + max-width: 100%; +} + + .uiSelectMatch--pillWithTooltip { + display: block; + margin-right: 16px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } diff --git a/src/ui/public/filter_editor/filter_field_select.html b/src/ui/public/filter_editor/filter_field_select.html new file mode 100644 index 00000000000000..65c45b1bae7b05 --- /dev/null +++ b/src/ui/public/filter_editor/filter_field_select.html @@ -0,0 +1,27 @@ + + + + {{$select.selected.name}} + + + +
+
+
diff --git a/src/ui/public/filter_editor/filter_field_select.js b/src/ui/public/filter_editor/filter_field_select.js new file mode 100644 index 00000000000000..f1aeb276d58d1d --- /dev/null +++ b/src/ui/public/filter_editor/filter_field_select.js @@ -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 'angular-ui-select'; +import { uiModules } from '../modules'; +import { getFilterableFields } from './lib/filter_editor_utils'; +import template from './filter_field_select.html'; +import '../directives/ui_select_focus_on'; +import '../directives/scroll_bottom'; +import '../filters/sort_prefix_first'; + +const module = uiModules.get('kibana'); +module.directive('filterFieldSelect', function () { + return { + restrict: 'E', + template, + scope: { + indexPatterns: '=', + field: '=', + onSelect: '&' + }, + link: function ($scope) { + $scope.$watch('indexPatterns', (indexPatterns) => { + $scope.fieldOptions = getFilterableFields(indexPatterns); + }); + + $scope.getFieldIndexPattern = (field) => { + return field.indexPattern.title; + }; + + $scope.increaseLimit = () => $scope.limit += 50; + $scope.resetLimit = () => $scope.limit = 50; + $scope.resetLimit(); + } + }; +}); diff --git a/src/ui/public/filter_editor/filter_operator_select.html b/src/ui/public/filter_editor/filter_operator_select.html new file mode 100644 index 00000000000000..78b730b93b58aa --- /dev/null +++ b/src/ui/public/filter_editor/filter_operator_select.html @@ -0,0 +1,12 @@ + + + {{$select.selected.name}} + + +
+
+
diff --git a/src/ui/public/filter_bar/directive.js b/src/ui/public/filter_editor/filter_operator_select.js similarity index 60% rename from src/ui/public/filter_bar/directive.js rename to src/ui/public/filter_editor/filter_operator_select.js index afb598de5a7b1b..3d877aee869b7a 100644 --- a/src/ui/public/filter_bar/directive.js +++ b/src/ui/public/filter_editor/filter_operator_select.js @@ -17,13 +17,26 @@ * under the License. */ -import 'ngreact'; +import 'angular-ui-select'; import { uiModules } from '../modules'; -import { FilterBar } from './filter_bar'; -import { injectI18nProvider } from '@kbn/i18n/react'; +import { getOperatorOptions } from './lib/filter_editor_utils'; +import template from './filter_operator_select.html'; +import '../directives/ui_select_focus_on'; -const app = uiModules.get('app/kibana', ['react']); - -app.directive('filterBar', reactDirective => { - return reactDirective(injectI18nProvider(FilterBar)); +const module = uiModules.get('kibana'); +module.directive('filterOperatorSelect', function () { + return { + restrict: 'E', + template, + scope: { + field: '=', + operator: '=', + onSelect: '&' + }, + link: function ($scope) { + $scope.$watch('field', (field) => { + $scope.operatorOptions = getOperatorOptions(field); + }); + } + }; }); diff --git a/src/ui/public/filter_editor/filter_query_dsl_editor.html b/src/ui/public/filter_editor/filter_query_dsl_editor.html new file mode 100644 index 00000000000000..999dae7c1df0cf --- /dev/null +++ b/src/ui/public/filter_editor/filter_query_dsl_editor.html @@ -0,0 +1,13 @@ +
diff --git a/src/ui/public/filter_editor/filter_query_dsl_editor.js b/src/ui/public/filter_editor/filter_query_dsl_editor.js new file mode 100644 index 00000000000000..16f763378ae05c --- /dev/null +++ b/src/ui/public/filter_editor/filter_query_dsl_editor.js @@ -0,0 +1,59 @@ +/* + * 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 'ace'; +import _ from 'lodash'; +import { uiModules } from '../modules'; +import template from './filter_query_dsl_editor.html'; +import '../accessibility/kbn_ui_ace_keyboard_mode'; + +const module = uiModules.get('kibana'); +module.directive('filterQueryDslEditor', function () { + return { + restrict: 'E', + template, + scope: { + isVisible: '=', + filter: '=', + onChange: '&' + }, + link: { + pre: function ($scope) { + let aceEditor; + + $scope.queryDsl = _.omit($scope.filter, ['meta', '$state']); + $scope.aceLoaded = function (editor) { + aceEditor = editor; + editor.$blockScrolling = Infinity; + const session = editor.getSession(); + session.setTabSize(2); + session.setUseSoftTabs(true); + }; + + $scope.$watch('isVisible', isVisible => { + // Tell the editor to re-render itself now that it's visible, otherwise it won't + // show up in the UI. + if (isVisible && aceEditor) { + aceEditor.renderer.updateFull(); + } + }); + } + } + }; +}); diff --git a/src/ui/public/documentation_links/index.d.ts b/src/ui/public/filter_editor/index.js similarity index 94% rename from src/ui/public/documentation_links/index.d.ts rename to src/ui/public/filter_editor/index.js index b37021b497de39..dc613de885ee7e 100644 --- a/src/ui/public/documentation_links/index.d.ts +++ b/src/ui/public/filter_editor/index.js @@ -17,4 +17,4 @@ * under the License. */ -export function getDocLink(id: string): string; +import './filter_editor'; diff --git a/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js b/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js new file mode 100644 index 00000000000000..61aadc4c074873 --- /dev/null +++ b/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js @@ -0,0 +1,396 @@ +/* + * 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 expect from 'expect.js'; +import ngMock from 'ng_mock'; +import sinon from 'sinon'; +import Promise from 'bluebird'; +import { + phraseFilter, + scriptedPhraseFilter, + phrasesFilter, + rangeFilter, + existsFilter +} from 'fixtures/filters'; +import stubbedLogstashIndexPattern from 'fixtures/stubbed_logstash_index_pattern'; +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { FILTER_OPERATORS } from '../filter_operators'; +import { + getQueryDslFromFilter, + getFieldFromFilter, + getOperatorFromFilter, + getParamsFromFilter, + getFilterableFields, + getOperatorOptions, + isFilterValid, + buildFilter, + areIndexPatternsProvided, + isFilterPinned +} from '../filter_editor_utils'; + +describe('FilterEditorUtils', function () { + beforeEach(ngMock.module('kibana')); + + let indexPattern; + let fields; + beforeEach(function () { + ngMock.inject(function (Private) { + indexPattern = Private(stubbedLogstashIndexPattern); + fields = stubbedLogstashFields(); + }); + }); + + describe('getQueryDslFromFilter', function () { + it('should return query DSL without meta and $state', function () { + const queryDsl = getQueryDslFromFilter(phraseFilter); + expect(queryDsl).to.not.have.key('meta'); + expect(queryDsl).to.not.have.key('$state'); + expect(queryDsl).to.have.key('query'); + }); + }); + + describe('getFieldFromFilter', function () { + let indexPatterns; + beforeEach(function () { + indexPatterns = { + get: sinon.stub().returns(Promise.resolve(indexPattern)) + }; + }); + + it('should return the field from the filter', function (done) { + getFieldFromFilter(phraseFilter, indexPatterns) + .then((field) => { + expect(field).to.be.ok(); + done(); + }); + }); + }); + + describe('getOperatorFromFilter', function () { + it('should return "is" for phrase filter', function () { + const operator = getOperatorFromFilter(phraseFilter); + expect(operator.name).to.be('is'); + expect(operator.negate).to.be(false); + }); + + it('should return "is not" for negated phrase filter', function () { + const negate = phraseFilter.meta.negate; + phraseFilter.meta.negate = true; + const operator = getOperatorFromFilter(phraseFilter); + expect(operator.name).to.be('is not'); + expect(operator.negate).to.be(true); + phraseFilter.meta.negate = negate; + }); + + it('should return "is one of" for phrases filter', function () { + const operator = getOperatorFromFilter(phrasesFilter); + expect(operator.name).to.be('is one of'); + expect(operator.negate).to.be(false); + }); + + it('should return "is not one of" for negated phrases filter', function () { + const negate = phrasesFilter.meta.negate; + phrasesFilter.meta.negate = true; + const operator = getOperatorFromFilter(phrasesFilter); + expect(operator.name).to.be('is not one of'); + expect(operator.negate).to.be(true); + phrasesFilter.meta.negate = negate; + }); + + it('should return "is between" for range filter', function () { + const operator = getOperatorFromFilter(rangeFilter); + expect(operator.name).to.be('is between'); + expect(operator.negate).to.be(false); + }); + + it('should return "is not between" for negated range filter', function () { + const negate = rangeFilter.meta.negate; + rangeFilter.meta.negate = true; + const operator = getOperatorFromFilter(rangeFilter); + expect(operator.name).to.be('is not between'); + expect(operator.negate).to.be(true); + rangeFilter.meta.negate = negate; + }); + + it('should return "exists" for exists filter', function () { + const operator = getOperatorFromFilter(existsFilter); + expect(operator.name).to.be('exists'); + expect(operator.negate).to.be(false); + }); + + it('should return "does not exists" for negated exists filter', function () { + const negate = existsFilter.meta.negate; + existsFilter.meta.negate = true; + const operator = getOperatorFromFilter(existsFilter); + expect(operator.name).to.be('does not exist'); + expect(operator.negate).to.be(true); + existsFilter.meta.negate = negate; + }); + }); + + describe('getParamsFromFilter', function () { + it('should retrieve params from phrase filter', function () { + const params = getParamsFromFilter(phraseFilter); + expect(params.phrase).to.be('ios'); + }); + + it('should retrieve params from scripted phrase filter', function () { + const params = getParamsFromFilter(scriptedPhraseFilter); + expect(params.phrase).to.be('i am a string'); + }); + + it('should retrieve params from phrases filter', function () { + const params = getParamsFromFilter(phrasesFilter); + expect(params.phrases).to.eql(['win xp', 'osx']); + }); + + it('should retrieve params from range filter', function () { + const params = getParamsFromFilter(rangeFilter); + expect(params.range).to.eql({ from: 0, to: 10 }); + }); + + it('should return undefined for exists filter', function () { + const params = getParamsFromFilter(existsFilter); + expect(params.exists).to.not.be.ok(); + }); + }); + + describe('getFilterableFields', function () { + it('returns an empty array when no index patterns are provided', function () { + const fieldOptions = getFilterableFields(); + expect(fieldOptions).to.eql([]); + }); + + it('returns the list of fields from the given index patterns', function () { + const fieldOptions = getFilterableFields([indexPattern]); + expect(fieldOptions).to.be.an('array'); + expect(fieldOptions.length).to.be.greaterThan(0); + }); + + it('limits the fields to the filterable fields', function () { + const fieldOptions = getFilterableFields([indexPattern]); + const nonFilterableFields = fieldOptions.filter(field => !field.filterable); + expect(nonFilterableFields.length).to.be(0); + }); + }); + + describe('getOperatorOptions', function () { + it('returns range for number fields', function () { + const field = fields.find(field => field.type === 'number'); + const operatorOptions = getOperatorOptions(field); + const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); + expect(rangeOperator).to.be.ok(); + }); + + it('does not return range for string fields', function () { + const field = fields.find(field => field.type === 'string'); + const operatorOptions = getOperatorOptions(field); + const rangeOperator = operatorOptions.find(operator => operator.type === 'range'); + expect(rangeOperator).to.not.be.ok(); + }); + + it('returns operators without field type restrictions', function () { + const operatorOptions = getOperatorOptions(); + const operatorsWithoutFieldTypes = FILTER_OPERATORS.filter(operator => !operator.fieldTypes); + expect(operatorOptions.length).to.be(operatorsWithoutFieldTypes.length); + }); + }); + + describe('isFilterValid', function () { + it('should return false if field is not provided', function () { + const field = null; + const operator = FILTER_OPERATORS[0]; + const params = { phrase: 'foo' }; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.not.be.ok(); + }); + + it('should return false if operator is not provided', function () { + const field = fields[0]; + const operator = null; + const params = { phrase: 'foo' }; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.not.be.ok(); + }); + + it('should return false for phrase filter without phrase', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'phrase'); + const params = {}; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.not.be.ok(); + }); + + it('should return true for phrase filter with phrase', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'phrase'); + const params = { phrase: 'foo' }; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.be.ok(); + }); + + it('should return false for phrases filter without phrases', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'phrases'); + const params = {}; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.not.be.ok(); + }); + + it('should return true for phrases filter with phrases', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'phrases'); + const params = { phrases: ['foo', 'bar'] }; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.be.ok(); + }); + + it('should return false for range filter without range', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'range'); + const params = {}; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.not.be.ok(); + }); + + it('should return true for range filter with from', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'range'); + const params = { range: { from: 0 } }; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.be.ok(); + }); + + it('should return true for range filter with from/to', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'range'); + const params = { range: { from: 0, to: 10 } }; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.be.ok(); + }); + + it('should return true for exists filter without params', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'exists'); + const params = {}; + const isValid = isFilterValid({ field, operator, params }); + expect(isValid).to.be.ok(); + }); + }); + + describe('buildFilter', function () { + let filterBuilder; + beforeEach(function () { + filterBuilder = { + buildExistsFilter: sinon.stub().returns(existsFilter), + buildPhraseFilter: sinon.stub().returns(phraseFilter), + buildPhrasesFilter: sinon.stub().returns(phrasesFilter), + buildRangeFilter: sinon.stub().returns(rangeFilter) + }; + }); + + it('should build phrase filters', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'phrase'); + const params = { phrase: 'foo' }; + const filter = buildFilter({ indexPattern, field, operator, params, filterBuilder }); + expect(filter).to.be.ok(); + expect(filter.meta.negate).to.be(operator.negate); + expect(filterBuilder.buildPhraseFilter.called).to.be.ok(); + expect(filterBuilder.buildPhraseFilter.getCall(0).args[1]).to.be(params.phrase); + }); + + it('should build phrases filters', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'phrases'); + const params = { phrases: ['foo', 'bar'] }; + const filter = buildFilter({ indexPattern, field, operator, params, filterBuilder }); + expect(filter).to.be.ok(); + expect(filter.meta.negate).to.be(operator.negate); + expect(filterBuilder.buildPhrasesFilter.called).to.be.ok(); + expect(filterBuilder.buildPhrasesFilter.getCall(0).args[1]).to.eql(params.phrases); + }); + + it('should build range filters', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'range'); + const params = { range: { from: 0, to: 10 } }; + const filter = buildFilter({ indexPattern, field, operator, params, filterBuilder }); + expect(filter).to.be.ok(); + expect(filter.meta.negate).to.be(operator.negate); + expect(filterBuilder.buildRangeFilter.called).to.be.ok(); + const range = filterBuilder.buildRangeFilter.getCall(0).args[1]; + expect(range).to.have.property('gte'); + expect(range).to.have.property('lt'); + }); + + it('should build exists filters', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'exists'); + const params = {}; + const filter = buildFilter({ indexPattern, field, operator, params, filterBuilder }); + expect(filter).to.be.ok(); + expect(filter.meta.negate).to.be(operator.negate); + expect(filterBuilder.buildExistsFilter.called).to.be.ok(); + }); + + it('should negate based on operator', function () { + const field = fields[0]; + const operator = FILTER_OPERATORS.find(operator => operator.type === 'exists' && operator.negate); + const params = {}; + const filter = buildFilter({ indexPattern, field, operator, params, filterBuilder }); + expect(filter).to.be.ok(); + expect(filter.meta.negate).to.be(operator.negate); + expect(filterBuilder.buildExistsFilter.called).to.be.ok(); + }); + }); + + describe('areIndexPatternsProvided', function () { + it('should return false when index patterns are not provided', function () { + expect(areIndexPatternsProvided(undefined)).to.be(false); + expect(areIndexPatternsProvided([])).to.be(false); + expect(areIndexPatternsProvided([undefined])).to.be(false); + }); + + it('should return true when index patterns are provided', function () { + const indexPatternMock = {}; + expect(areIndexPatternsProvided([indexPatternMock])).to.be(true); + }); + }); + + describe('isFilterPinned', function () { + it('should return false when the store is appState', function () { + const filter = { $state: { store: 'appState' } }; + expect(isFilterPinned(filter, false)).to.be(false); + expect(isFilterPinned(filter, true)).to.be(false); + }); + + it('should return true when the store is globalState', function () { + const filter = { $state: { store: 'globalState' } }; + expect(isFilterPinned(filter, false)).to.be(true); + expect(isFilterPinned(filter, true)).to.be(true); + }); + + it('should return the default when the store does not exist', function () { + const filter = {}; + expect(isFilterPinned(filter, false)).to.be(false); + expect(isFilterPinned(filter, true)).to.be(true); + }); + }); +}); diff --git a/src/ui/public/filter_editor/lib/filter_editor_utils.js b/src/ui/public/filter_editor/lib/filter_editor_utils.js new file mode 100644 index 00000000000000..4c0067b62193dc --- /dev/null +++ b/src/ui/public/filter_editor/lib/filter_editor_utils.js @@ -0,0 +1,111 @@ +/* + * 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 _ from 'lodash'; +import { FILTER_OPERATORS } from './filter_operators'; + +export function getQueryDslFromFilter(filter) { + return _(filter) + .omit(['meta', '$state']) + .cloneDeep(); +} + +export function getFieldFromFilter(filter, indexPatterns) { + const { index, key } = filter.meta; + return indexPatterns.get(index) + .then(indexPattern => indexPattern.id && indexPattern.fields.byName[key]); +} + +export function getOperatorFromFilter(filter) { + const { type, negate } = filter.meta; + return FILTER_OPERATORS.find((operator) => { + return operator.type === type && operator.negate === negate; + }); +} + +export function getParamsFromFilter(filter) { + const { type, key } = filter.meta; + let params; + if (type === 'phrase') { + params = filter.query ? filter.query.match[key].query : filter.script.script.params.value; + } else if (type === 'phrases') { + params = filter.meta.params; + } else if (type === 'range') { + const range = filter.range ? filter.range[key] : filter.script.script.params; + const from = _.has(range, 'gte') ? range.gte : range.gt; + const to = _.has(range, 'lte') ? range.lte : range.lt; + params = { from, to }; + } + return { + [type]: params + }; +} + +export function getFilterableFields(indexPatterns) { + return (indexPatterns || []).reduce((fields, indexPattern) => { + const filterableFields = indexPattern.fields.filter(field => field.filterable); + return [...fields, ...filterableFields]; + }, []); +} + +export function getOperatorOptions(field) { + const type = _.get(field, 'type'); + return FILTER_OPERATORS.filter((operator) => { + return !operator.fieldTypes || operator.fieldTypes.includes(type); + }); +} + +export function isFilterValid({ field, operator, params }) { + if (!field || !operator) { + return false; + } else if (operator.type === 'phrase') { + return _.has(params, 'phrase') && params.phrase !== ''; + } else if (operator.type === 'phrases') { + return _.has(params, 'phrases') && params.phrases.length > 0; + } else if (operator.type === 'range') { + const hasFrom = _.has(params, ['range', 'from']) && params.range.from !== ''; + const hasTo = _.has(params, ['range', 'to']) && params.range.to !== ''; + return hasFrom || hasTo; + } + return true; +} + +export function buildFilter({ indexPattern, field, operator, params, filterBuilder }) { + let filter; + if (operator.type === 'phrase') { + filter = filterBuilder.buildPhraseFilter(field, params.phrase, indexPattern); + } else if (operator.type === 'phrases') { + filter = filterBuilder.buildPhrasesFilter(field, params.phrases, indexPattern); + } else if (operator.type === 'range') { + filter = filterBuilder.buildRangeFilter(field, { gte: params.range.from, lt: params.range.to }, indexPattern); + } else if (operator.type === 'exists') { + filter = filterBuilder.buildExistsFilter(field, indexPattern); + } + filter.meta.negate = operator.negate; + return filter; +} + +export function areIndexPatternsProvided(indexPatterns) { + return _.compact(indexPatterns).length !== 0; +} + +export function isFilterPinned(filter, pinnedByDefault) { + if (!filter.hasOwnProperty('$state')) return pinnedByDefault; + return filter.$state.store === 'globalState'; +} diff --git a/src/ui/public/filter_editor/lib/filter_operators.js b/src/ui/public/filter_editor/lib/filter_operators.js new file mode 100644 index 00000000000000..140928eb1912d8 --- /dev/null +++ b/src/ui/public/filter_editor/lib/filter_operators.js @@ -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. + */ + +import _ from 'lodash'; + +export const FILTER_OPERATORS = [ + { + name: 'is', + type: 'phrase', + negate: false, + }, + { + name: 'is not', + type: 'phrase', + negate: true, + }, + { + name: 'is one of', + type: 'phrases', + negate: false, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'] + }, + { + name: 'is not one of', + type: 'phrases', + negate: true, + fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape'] + }, + { + name: 'is between', + type: 'range', + negate: false, + fieldTypes: ['number', 'date', 'ip'], + }, + { + name: 'is not between', + type: 'range', + negate: true, + fieldTypes: ['number', 'date', 'ip'], + }, + { + name: 'exists', + type: 'exists', + negate: false, + }, + { + name: 'does not exist', + type: 'exists', + negate: true, + }, +]; + +export const FILTER_OPERATOR_TYPES = _(FILTER_OPERATORS) + .map('type') + .uniq() + .value(); diff --git a/src/ui/public/filter_editor/params_editor/filter_params_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_editor.html new file mode 100644 index 00000000000000..f242ce2af5a2f6 --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_editor.html @@ -0,0 +1,19 @@ + + + + + + + diff --git a/packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts b/src/ui/public/filter_editor/params_editor/filter_params_editor.js similarity index 65% rename from packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts rename to src/ui/public/filter_editor/params_editor/filter_params_editor.js index 80b606e13bb3de..d0397d4f7508e3 100644 --- a/packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts +++ b/src/ui/public/filter_editor/params_editor/filter_params_editor.js @@ -17,14 +17,21 @@ * under the License. */ -import { Filter, FilterMeta, LatLon } from './meta_filter'; +import { uiModules } from '../../modules'; +import template from './filter_params_editor.html'; +import './filter_params_phrase_editor'; +import './filter_params_phrases_editor'; +import './filter_params_range_editor'; -export type GeoPolygonFilterMeta = FilterMeta & { - params: { - points: LatLon[]; +const module = uiModules.get('kibana'); +module.directive('filterParamsEditor', function () { + return { + restrict: 'E', + template, + scope: { + field: '=', + operator: '=', + params: '=' + } }; -}; - -export type GeoPolygonFilter = Filter & { - meta: GeoPolygonFilterMeta; -}; +}); diff --git a/src/ui/public/filter_editor/params_editor/filter_params_input_type.html b/src/ui/public/filter_editor/params_editor/filter_params_input_type.html new file mode 100644 index 00000000000000..1d1a89bbb0df98 --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_input_type.html @@ -0,0 +1,50 @@ +
+ + + + +
+ +
+
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_input_type.js b/src/ui/public/filter_editor/params_editor/filter_params_input_type.js new file mode 100644 index 00000000000000..1278e8b508be3d --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_input_type.js @@ -0,0 +1,49 @@ +/* + * 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 { uiModules } from '../../modules'; +import template from './filter_params_input_type.html'; +import '../../directives/validate_date_math'; +import '../../directives/validate_ip'; +import '../../directives/string_to_number'; + +const module = uiModules.get('kibana'); +module.directive('filterParamsInputType', function () { + return { + restrict: 'E', + template, + scope: { + type: '=', + placeholder: '@', + value: '=', + onChange: '&' + }, + link: function (scope) { + scope.boolOptions = [true, false]; + scope.setDefaultBool = () => { + if (scope.value == null) { + scope.value = scope.boolOptions[0]; + scope.onChange({ + value: scope.value + }); + } + }; + } + }; +}); diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js b/src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js new file mode 100644 index 00000000000000..cbcd2d8ab5223a --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js @@ -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 _ from 'lodash'; +import chrome from '../../chrome'; + +const baseUrl = chrome.addBasePath('/api/kibana/suggestions/values'); + +export function filterParamsPhraseController($http, $scope, config) { + const shouldSuggestValues = this.shouldSuggestValues = config.get('filterEditor:suggestValues'); + + this.compactUnion = _.flow(_.union, _.compact); + + this.getValueSuggestions = _.memoize(getValueSuggestions, getFieldQueryHash); + + this.refreshValueSuggestions = (query) => { + return this.getValueSuggestions($scope.field, query) + .then(suggestions => $scope.valueSuggestions = suggestions); + }; + + this.refreshValueSuggestions(); + + function getValueSuggestions(field, query) { + if (!shouldSuggestValues || !_.get(field, 'aggregatable') || field.type !== 'string') { + return Promise.resolve([]); + } + + const params = { + query, + field: field.name + }; + + return $http.post(`${baseUrl}/${field.indexPattern.title}`, params) + .then(response => response.data) + .catch(() => []); + } + + function getFieldQueryHash(field, query = '') { + return `${field.indexPattern.id}/${field.name}/${query}`; + } +} diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html new file mode 100644 index 00000000000000..36d509f28decef --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html @@ -0,0 +1,45 @@ + + + + {{$select.selected}} + + + +
+
+
+ + + + + + Accepted date formats + + diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js new file mode 100644 index 00000000000000..d7b166e7990f74 --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js @@ -0,0 +1,42 @@ +/* + * 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 'angular-ui-select'; +import { uiModules } from '../../modules'; +import template from './filter_params_phrase_editor.html'; +import { filterParamsPhraseController } from './filter_params_phrase_controller'; +import './filter_params_input_type'; +import '../../directives/documentation_href'; +import '../../directives/ui_select_focus_on'; +import '../../directives/focus_on'; +import '../../filters/sort_prefix_first'; + +const module = uiModules.get('kibana'); +module.directive('filterParamsPhraseEditor', function () { + return { + restrict: 'E', + template, + scope: { + field: '=', + params: '=' + }, + controllerAs: 'filterParamsPhraseEditor', + controller: filterParamsPhraseController + }; +}); diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html new file mode 100644 index 00000000000000..4a2935c4ba0b71 --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html @@ -0,0 +1,28 @@ + + + + {{$item}} + + + +
+
+
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js new file mode 100644 index 00000000000000..ca4f67b28caf57 --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js @@ -0,0 +1,39 @@ +/* + * 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 'angular-ui-select'; +import { uiModules } from '../../modules'; +import template from './filter_params_phrases_editor.html'; +import { filterParamsPhraseController } from './filter_params_phrase_controller'; +import '../../directives/ui_select_focus_on'; +import '../../filters/sort_prefix_first'; + +const module = uiModules.get('kibana'); +module.directive('filterParamsPhrasesEditor', function () { + return { + restrict: 'E', + template, + scope: { + field: '=', + params: '=' + }, + controllerAs: 'filterParamsPhrasesEditor', + controller: filterParamsPhraseController + }; +}); diff --git a/src/ui/public/filter_editor/params_editor/filter_params_range_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.html new file mode 100644 index 00000000000000..483d6c7408bd85 --- /dev/null +++ b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.html @@ -0,0 +1,30 @@ +
+ +
+ +
+ +
+ + + + Accepted date formats + + diff --git a/src/ui/public/search_bar/directive/index.js b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.js similarity index 69% rename from src/ui/public/search_bar/directive/index.js rename to src/ui/public/filter_editor/params_editor/filter_params_range_editor.js index f03624dd189a23..8bab49a1bbe0a6 100644 --- a/src/ui/public/search_bar/directive/index.js +++ b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.js @@ -17,20 +17,20 @@ * under the License. */ -import 'ngreact'; import { uiModules } from '../../modules'; -import { SearchBar } from '../components'; -import { injectI18nProvider } from '@kbn/i18n/react'; +import template from './filter_params_range_editor.html'; +import './filter_params_input_type'; +import '../../directives/documentation_href'; +import '../../directives/focus_on'; -const app = uiModules.get('app/kibana', ['react']); - -app.directive('searchBar', (reactDirective, localStorage) => { - return reactDirective( - injectI18nProvider(SearchBar), - undefined, - {}, - { - store: localStorage, +const module = uiModules.get('kibana'); +module.directive('filterParamsRangeEditor', function () { + return { + restrict: 'E', + template, + scope: { + field: '=', + params: '=' } - ); + }; }); diff --git a/src/ui/public/index_patterns/_field.d.ts b/src/ui/public/index_patterns/_field.d.ts deleted file mode 100644 index 749cd63d0a84cd..00000000000000 --- a/src/ui/public/index_patterns/_field.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 Field { - name: string; - type: string; - aggregatable: boolean; - filterable: boolean; - searchable: boolean; -} diff --git a/src/ui/public/index_patterns/_index_pattern.d.ts b/src/ui/public/index_patterns/_index_pattern.d.ts index 0465e35ff2a1fe..0ff899839c42cc 100644 --- a/src/ui/public/index_patterns/_index_pattern.d.ts +++ b/src/ui/public/index_patterns/_index_pattern.d.ts @@ -17,18 +17,11 @@ * under the License. */ -import { Field } from 'ui/index_patterns/_field'; - /** * WARNING: these types are incomplete */ -export interface IndexPattern { - id: string; - fields: Field[]; - title: string; - timeFieldName?: string; -} +export type IndexPattern = any; export interface StaticIndexPatternField { name: string; diff --git a/src/ui/public/index_patterns/fixtures/index.ts b/src/ui/public/index_patterns/fixtures/index.ts deleted file mode 100644 index a84ab381b52d8b..00000000000000 --- a/src/ui/public/index_patterns/fixtures/index.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 { Field, IndexPattern } from '../index'; - -export const mockFields: Field[] = [ - { - name: 'machine.os', - type: 'string', - aggregatable: false, - searchable: false, - filterable: true, - }, - { - name: 'machine.os.raw', - type: 'string', - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: 'not.filterable', - type: 'string', - aggregatable: true, - searchable: false, - filterable: false, - }, - { - name: 'bytes', - type: 'number', - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: '@timestamp', - type: 'date', - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: 'clientip', - type: 'ip', - aggregatable: true, - searchable: true, - filterable: true, - }, - { - name: 'bool.field', - type: 'boolean', - aggregatable: true, - searchable: true, - filterable: true, - }, -]; - -export const mockIndexPattern: IndexPattern = { - id: 'logstash-*', - fields: mockFields, - title: 'logstash-*', - timeFieldName: '@timestamp', -}; diff --git a/src/ui/public/index_patterns/index.d.ts b/src/ui/public/index_patterns/index.d.ts index e2d7ddd8c5254c..92f04543c237e2 100644 --- a/src/ui/public/index_patterns/index.d.ts +++ b/src/ui/public/index_patterns/index.d.ts @@ -17,9 +17,4 @@ * under the License. */ -export { - IndexPattern, - StaticIndexPattern, - StaticIndexPatternField, -} from 'ui/index_patterns/_index_pattern'; -export { Field } from 'ui/index_patterns/_field'; +export { IndexPattern, StaticIndexPattern } from 'ui/index_patterns/_index_pattern'; diff --git a/src/ui/public/index_patterns/static_utils/__tests__/index.js b/src/ui/public/index_patterns/static_utils/__tests__/index.js index e825df5d294cad..4a81b35e96555a 100644 --- a/src/ui/public/index_patterns/static_utils/__tests__/index.js +++ b/src/ui/public/index_patterns/static_utils/__tests__/index.js @@ -20,50 +20,26 @@ import expect from 'expect.js'; import { isFilterable } from '../index'; -const mockField = { - name: 'foo', - scripted: false, - searchable: true, - type: 'string', -}; - describe('static utils', () => { describe('isFilterable', () => { - describe('types', () => { - it('should return true for filterable types', () => { - ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { - expect(isFilterable({ ...mockField, type })).to.be(true); - }); - }); - - it('should return false for filterable types if the field is not searchable', () => { - ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { - expect(isFilterable({ ...mockField, type, searchable: false })).to.be(false); - }); - }); - - it('should return false for un-filterable types', () => { - [ - 'geo_point', - 'geo_shape', - 'attachment', - 'murmur3', - '_source', - 'unknown', - 'conflict', - ].forEach(type => { - expect(isFilterable({ ...mockField, type })).to.be(false); - }); + it('should be filterable', () => { + ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { + expect(isFilterable({ type })).to.be(true); }); }); - - it('should return true for scripted fields', () => { - expect(isFilterable({ ...mockField, scripted: true, searchable: false })).to.be(true); - }); - - it('should return true for the _id field', () => { - expect(isFilterable({ ...mockField, name: '_id' })).to.be(true); + it('should not be filterable', () => { + [ + 'geo_point', + 'geo_shape', + 'attachment', + 'murmur3', + '_source', + 'unknown', + 'conflict', + ].forEach(type => { + expect(isFilterable({ type })).to.be(false); + }); }); }); }); diff --git a/src/ui/public/index_patterns/static_utils/index.d.ts b/src/ui/public/index_patterns/static_utils/index.d.ts index c3e02f2b20d79a..6d387bb95882f5 100644 --- a/src/ui/public/index_patterns/static_utils/index.d.ts +++ b/src/ui/public/index_patterns/static_utils/index.d.ts @@ -17,6 +17,13 @@ * under the License. */ -import { Field } from 'ui/index_patterns'; +import { StaticIndexPattern } from 'ui/index_patterns'; -export function isFilterable(field: Field): boolean; +interface SavedObject { + attributes: { + fields: string; + title: string; + }; +} + +export function getFromLegacyIndexPattern(indexPatterns: any[]): StaticIndexPattern[]; diff --git a/src/ui/public/index_patterns/static_utils/index.js b/src/ui/public/index_patterns/static_utils/index.js index 2285858c40cccd..2cf43c319b10ae 100644 --- a/src/ui/public/index_patterns/static_utils/index.js +++ b/src/ui/public/index_patterns/static_utils/index.js @@ -22,7 +22,7 @@ import { KBN_FIELD_TYPES } from '../../../../utils/kbn_field_types'; const filterableTypes = KBN_FIELD_TYPES.filter(type => type.filterable).map(type => type.name); export function isFilterable(field) { - return field.name === '_id' || field.scripted || (field.searchable && filterableTypes.includes(field.type)); + return filterableTypes.includes(field.type); } export function getFromSavedObject(savedObject) { @@ -31,8 +31,14 @@ export function getFromSavedObject(savedObject) { } return { - id: savedObject.id, fields: JSON.parse(savedObject.attributes.fields), title: savedObject.attributes.title, }; } + +export function getFromLegacyIndexPattern(indexPatterns) { + return indexPatterns.map(indexPattern => ({ + fields: indexPattern.fields.raw, + title: indexPattern.title, + })); +} diff --git a/src/ui/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap b/src/ui/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap index e3465e4b50c514..fb092e9f684c01 100644 --- a/src/ui/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap +++ b/src/ui/public/query_bar/components/__snapshots__/query_bar.test.tsx.snap @@ -34,6 +34,7 @@ exports[`QueryBar Should disable autoFocus on EuiFieldText when disableAutoFocus role="form" >
({ }); const mockIndexPattern = { - id: '1234', title: 'logstash-*', - fields: [ - { - name: 'response', - type: 'number', - aggregatable: true, - filterable: true, - searchable: true, - }, - ], + fields: { + raw: [ + { + name: 'response', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + }, }; describe('QueryBar', () => { diff --git a/src/ui/public/query_bar/components/query_bar.tsx b/src/ui/public/query_bar/components/query_bar.tsx index 401950f8adf273..af91fea37eb437 100644 --- a/src/ui/public/query_bar/components/query_bar.tsx +++ b/src/ui/public/query_bar/components/query_bar.tsx @@ -21,6 +21,7 @@ import { IndexPattern } from 'ui/index_patterns'; import { compact, debounce, isEqual } from 'lodash'; import React, { Component } from 'react'; +import { getFromLegacyIndexPattern } from 'ui/index_patterns/static_utils'; import { kfetch } from 'ui/kfetch'; import { PersistedLog } from 'ui/persisted_log'; import { Storage } from 'ui/storage'; @@ -73,7 +74,6 @@ interface Props { indexPatterns: IndexPattern[]; store: Storage; intl: InjectedIntl; - prepend?: any; } interface State { @@ -196,7 +196,7 @@ export class QueryBarUI extends Component { return recentSearchSuggestions; } - const indexPatterns = this.props.indexPatterns; + const indexPatterns = getFromLegacyIndexPattern(this.props.indexPatterns); const getAutocompleteSuggestions = autocompleteProvider({ config, indexPatterns }); const { selectionStart, selectionEnd } = this.inputRef; @@ -476,7 +476,7 @@ export class QueryBarUI extends Component { aria-controls="typeahead-items" >
-
+
{ this.state.isSuggestionsVisible ? 'suggestion-' + this.state.index : '' } role="textbox" - prepend={this.props.prepend} />
void; - onDisableAll: () => void; - onPinAll: () => void; - onUnpinAll: () => void; - onToggleAllNegated: () => void; - onToggleAllDisabled: () => void; - onRemoveAll: () => void; - intl: InjectedIntl; -} - -interface State { - isPopoverOpen: boolean; -} - -class FilterOptionsUI 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: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.enableAllFiltersButtonLabel', - defaultMessage: 'Enable all', - }), - icon: 'eye', - onClick: () => { - this.closePopover(); - this.props.onEnableAll(); - }, - 'data-test-subj': 'enableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.disableAllFiltersButtonLabel', - defaultMessage: 'Disable all', - }), - icon: 'eyeClosed', - onClick: () => { - this.closePopover(); - this.props.onDisableAll(); - }, - 'data-test-subj': 'disableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.pinAllFiltersButtonLabel', - defaultMessage: 'Pin all', - }), - icon: 'pin', - onClick: () => { - this.closePopover(); - this.props.onPinAll(); - }, - 'data-test-subj': 'pinAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.unpinAllFiltersButtonLabel', - defaultMessage: 'Unpin all', - }), - icon: 'pin', - onClick: () => { - this.closePopover(); - this.props.onUnpinAll(); - }, - 'data-test-subj': 'unpinAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.invertNegatedFiltersButtonLabel', - defaultMessage: 'Invert inclusion', - }), - icon: 'invert', - onClick: () => { - this.closePopover(); - this.props.onToggleAllNegated(); - }, - 'data-test-subj': 'invertInclusionAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.invertDisabledFiltersButtonLabel', - defaultMessage: 'Invert enabled/disabled', - }), - icon: 'eye', - onClick: () => { - this.closePopover(); - this.props.onToggleAllDisabled(); - }, - 'data-test-subj': 'invertEnableDisableAllFilters', - }, - { - name: this.props.intl.formatMessage({ - id: 'common.ui.searchBar.deleteAllFiltersButtonLabel', - defaultMessage: 'Remove all', - }), - icon: 'trash', - onClick: () => { - this.closePopover(); - this.props.onRemoveAll(); - }, - 'data-test-subj': 'removeAllFilters', - }, - ], - }; - - return ( - - } - anchorPosition="rightUp" - panelPaddingSize="none" - withTitle - > - - - - - - ); - } -} - -export const FilterOptions = injectI18n(FilterOptionsUI); diff --git a/src/ui/public/search_bar/components/search_bar.tsx b/src/ui/public/search_bar/components/search_bar.tsx deleted file mode 100644 index ac7d5399e71446..00000000000000 --- a/src/ui/public/search_bar/components/search_bar.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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 { Filter } from '@kbn/es-query'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import classNames from 'classnames'; -import React, { Component } from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; -import { FilterBar } from 'ui/filter_bar'; -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: Filter[]; - onFiltersUpdated: (filters: Filter[]) => void; - showQueryBar: boolean; - showFilterBar: boolean; - intl: InjectedIntl; -} - -interface State { - isFiltersVisible: boolean; -} - -class SearchBarUI extends Component { - public static defaultProps = { - showQueryBar: true, - showFilterBar: true, - }; - - 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 : 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 filtersAppliedText = this.props.intl.formatMessage({ - id: 'common.ui.searchBar.filtersButtonFiltersAppliedTitle', - defaultMessage: 'filters applied.', - }); - const clickToShowOrHideText = this.state.isFiltersVisible - ? this.props.intl.formatMessage({ - id: 'common.ui.searchBar.filtersButtonClickToShowTitle', - defaultMessage: 'Select to hide', - }) - : this.props.intl.formatMessage({ - id: 'common.ui.searchBar.filtersButtonClickToHideTitle', - defaultMessage: 'Select to show', - }); - - const filterTriggerButton = ( - 0 ? this.props.filters.length : null} - aria-controls="GlobalFilterGroup" - aria-expanded={!!this.state.isFiltersVisible} - title={`${this.props.filters.length} ${filtersAppliedText} ${clickToShowOrHideText}`} - > - Filters - - ); - - const classes = classNames('globalFilterGroup__wrapper', { - 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, - }); - - return ( -
- {this.props.showQueryBar ? ( - - ) : ( - '' - )} - - {this.props.showFilterBar ? ( -
{ - this.filterBarWrapperRef = node; - }} - className={classes} - > -
{ - this.filterBarRef = node; - }} - > - -
-
- ) : ( - '' - )} -
- ); - } -} - -export const SearchBar = injectI18n(SearchBarUI); diff --git a/src/ui/public/styles/bootstrap_dark.less b/src/ui/public/styles/bootstrap_dark.less index 0e1dd7b017825f..51273dcc0e83b5 100644 --- a/src/ui/public/styles/bootstrap_dark.less +++ b/src/ui/public/styles/bootstrap_dark.less @@ -4,4 +4,5 @@ @import "~ui/styles/bootstrap/bootstrap_dark"; // Components -- waiting on EUI conversion +@import "~ui/filter_bar/filter_bar"; @import "~ui/timepicker/timepicker"; diff --git a/src/ui/public/timefilter/get_time.ts b/src/ui/public/timefilter/get_time.ts index 8dd3545c85a24c..4baff33e7e661f 100644 --- a/src/ui/public/timefilter/get_time.ts +++ b/src/ui/public/timefilter/get_time.ts @@ -18,7 +18,8 @@ */ import dateMath from '@elastic/datemath'; -import { Field, IndexPattern } from 'ui/index_patterns'; +import { find } from 'lodash'; +import { IndexPattern } from 'ui/index_patterns'; interface CalculateBoundsOptions { forceNow?: Date; @@ -57,9 +58,8 @@ export function getTime( } let filter: Filter; - const timefield: Field | undefined = indexPattern.fields.find( - field => field.name === indexPattern.timeFieldName - ); + const timefield: { name: string } | undefined = + indexPattern.timeFieldName && find(indexPattern.fields, { name: indexPattern.timeFieldName }); if (!timefield) { return; diff --git a/src/ui/public/value_suggestions/index.ts b/src/ui/public/value_suggestions/index.ts deleted file mode 100644 index 87ff473eaeece3..00000000000000 --- a/src/ui/public/value_suggestions/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 chrome from 'ui/chrome'; -import { kfetch } from 'ui/kfetch'; -import { getSuggestionsProvider } from './value_suggestions'; - -export const getSuggestions = getSuggestionsProvider(chrome.getUiSettingsClient(), kfetch); diff --git a/src/ui/public/value_suggestions/value_suggestions.test.ts b/src/ui/public/value_suggestions/value_suggestions.test.ts deleted file mode 100644 index e918a8669bfe7d..00000000000000 --- a/src/ui/public/value_suggestions/value_suggestions.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 { mockFields, mockIndexPattern } from 'ui/index_patterns/fixtures'; -import { getSuggestionsProvider } from './value_suggestions'; - -describe('getSuggestions', () => { - let getSuggestions: any; - let fetch: any; - - describe('with value suggestions disabled', () => { - beforeEach(() => { - const config = { get: () => false }; - fetch = jest.fn(); - getSuggestions = getSuggestionsProvider(config, fetch); - }); - - it('should return an empty array', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields; - const query = ''; - const suggestions = await getSuggestions(index, field, query); - expect(suggestions).toEqual([]); - expect(fetch).not.toHaveBeenCalled(); - }); - }); - - describe('with value suggestions enabled', () => { - beforeEach(() => { - const config = { get: () => true }; - fetch = jest.fn(); - getSuggestions = getSuggestionsProvider(config, fetch); - }); - - it('should return true/false for boolean fields', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter(({ type }) => type === 'boolean'); - const query = ''; - const suggestions = await getSuggestions(index, field, query); - expect(suggestions).toEqual([true, false]); - expect(fetch).not.toHaveBeenCalled(); - }); - - it('should return an empty array if the field type is not a string or boolean', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter(({ type }) => type !== 'string' && type !== 'boolean'); - const query = ''; - const suggestions = await getSuggestions(index, field, query); - expect(suggestions).toEqual([]); - expect(fetch).not.toHaveBeenCalled(); - }); - - it('should return an empty array if the field is not aggregatable', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter(({ aggregatable }) => !aggregatable); - const query = ''; - const suggestions = await getSuggestions(index, field, query); - expect(suggestions).toEqual([]); - expect(fetch).not.toHaveBeenCalled(); - }); - - it('should otherwise request suggestions', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter( - ({ type, aggregatable }) => type === 'string' && aggregatable - ); - const query = ''; - await getSuggestions(index, field, query); - expect(fetch).toHaveBeenCalled(); - }); - - it('should cache results if using the same index/field/query/filter', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter( - ({ type, aggregatable }) => type === 'string' && aggregatable - ); - const query = ''; - await getSuggestions(index, field, query); - await getSuggestions(index, field, query); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - it('should cache results for only one minute', async () => { - const index = mockIndexPattern.id; - const [field] = mockFields.filter( - ({ type, aggregatable }) => type === 'string' && aggregatable - ); - const query = ''; - - const { now } = Date; - Date.now = jest.fn(() => 0); - await getSuggestions(index, field, query); - Date.now = jest.fn(() => 60 * 1000); - await getSuggestions(index, field, query); - Date.now = now; - - expect(fetch).toHaveBeenCalledTimes(2); - }); - - it('should not cache results if using a different index/field/query', async () => { - const fields = mockFields.filter( - ({ type, aggregatable }) => type === 'string' && aggregatable - ); - await getSuggestions('index', fields[0], ''); - await getSuggestions('index', fields[0], 'query'); - await getSuggestions('index', fields[1], ''); - await getSuggestions('index', fields[1], 'query'); - await getSuggestions('logstash-*', fields[0], ''); - await getSuggestions('logstash-*', fields[0], 'query'); - await getSuggestions('logstash-*', fields[1], ''); - await getSuggestions('logstash-*', fields[1], 'query'); - expect(fetch).toHaveBeenCalledTimes(8); - }); - }); -}); diff --git a/src/ui/public/value_suggestions/value_suggestions.ts b/src/ui/public/value_suggestions/value_suggestions.ts deleted file mode 100644 index 31e42e9945ede0..00000000000000 --- a/src/ui/public/value_suggestions/value_suggestions.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 { Field } from 'ui/index_patterns'; - -export function getSuggestionsProvider( - config: { get: (key: string) => any }, - fetch: (...options: any[]) => any -) { - const requestSuggestions = memoize( - (index: string, field: Field, query: string, boolFilter: any = []) => { - return fetch({ - pathname: `/api/kibana/suggestions/values/${index}`, - method: 'POST', - body: JSON.stringify({ query, field: field.name, boolFilter }), - }); - }, - resolver - ); - - return async (index: string, field: Field, 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: Field, 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/src/ui/public/vis/editors/config/editor_config_providers.test.ts b/src/ui/public/vis/editors/config/editor_config_providers.test.ts index 94eb453ff54ed3..3ba4c78b0abb5c 100644 --- a/src/ui/public/vis/editors/config/editor_config_providers.test.ts +++ b/src/ui/public/vis/editors/config/editor_config_providers.test.ts @@ -22,19 +22,6 @@ import { EditorParamConfig, FixedParam, NumericIntervalParam, TimeIntervalParam describe('EditorConfigProvider', () => { let registry: EditorConfigProviderRegistry; - const indexPattern = { - id: '1234', - title: 'logstash-*', - fields: [ - { - name: 'response', - type: 'number', - aggregatable: true, - filterable: true, - searchable: true, - }, - ], - }; beforeEach(() => { registry = new EditorConfigProviderRegistry(); @@ -45,6 +32,7 @@ describe('EditorConfigProvider', () => { registry.register(provider); expect(provider).not.toHaveBeenCalled(); const aggType = {}; + const indexPattern = {}; const aggConfig = {}; registry.getConfigForAgg(aggType, indexPattern, aggConfig); expect(provider).toHaveBeenCalledWith(aggType, indexPattern, aggConfig); @@ -58,6 +46,7 @@ describe('EditorConfigProvider', () => { expect(provider).not.toHaveBeenCalled(); expect(provider2).not.toHaveBeenCalled(); const aggType = {}; + const indexPattern = {}; const aggConfig = {}; registry.getConfigForAgg(aggType, indexPattern, aggConfig); expect(provider).toHaveBeenCalledWith(aggType, indexPattern, aggConfig); @@ -70,7 +59,7 @@ describe('EditorConfigProvider', () => { } function getOutputConfig(reg: EditorConfigProviderRegistry) { - return reg.getConfigForAgg({}, indexPattern, {}).singleParam; + return reg.getConfigForAgg({}, {}, {}).singleParam; } it('should have hidden true if at least one config was hidden true', () => { diff --git a/test/functional/apps/dashboard/_dashboard_filter_bar.js b/test/functional/apps/dashboard/_dashboard_filter_bar.js index a393a949377e1c..bd074d896329da 100644 --- a/test/functional/apps/dashboard/_dashboard_filter_bar.js +++ b/test/functional/apps/dashboard/_dashboard_filter_bar.js @@ -58,7 +58,7 @@ export default function ({ getService, getPageObjects }) { it('uses default index pattern on an empty dashboard', async () => { await testSubjects.click('addFilter'); - await dashboardExpect.fieldSuggestions(['bytes']); + await dashboardExpect.fieldSuggestionIndexPatterns(['logstash-*']); }); it('shows index pattern of vis when one is added', async () => { @@ -66,7 +66,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); await filterBar.ensureFieldEditorModalIsClosed(); await testSubjects.click('addFilter'); - await dashboardExpect.fieldSuggestions(['animal']); + await dashboardExpect.fieldSuggestionIndexPatterns(['animals-*']); }); it('works when a vis with no index pattern is added', async () => { @@ -74,7 +74,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); await filterBar.ensureFieldEditorModalIsClosed(); await testSubjects.click('addFilter'); - await dashboardExpect.fieldSuggestions(['animal']); + await dashboardExpect.fieldSuggestionIndexPatterns(['animals-*']); }); }); @@ -86,16 +86,16 @@ export default function ({ getService, getPageObjects }) { }); it('are not selected by default', async function () { - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.equal(0); + const filters = await PageObjects.dashboard.getFilters(1000); + expect(filters.length).to.equal(0); }); it('are added when a pie chart slice is clicked', async function () { await dashboardAddPanel.addVisualization('Rendering Test: pie'); await PageObjects.dashboard.waitForRenderComplete(); await pieChart.filterOnPieSlice('4,886'); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.equal(1); + const filters = await PageObjects.dashboard.getFilters(); + expect(filters.length).to.equal(1); await pieChart.expectPieSliceCount(1); }); @@ -104,8 +104,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.saveDashboard('with filters'); await PageObjects.header.waitUntilLoadingHasFinished(); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.equal(1); + const filters = await PageObjects.dashboard.getFilters(); + expect(filters.length).to.equal(1); await pieChart.expectPieSliceCount(1); }); @@ -115,8 +115,8 @@ export default function ({ getService, getPageObjects }) { await PageObjects.dashboard.loadSavedDashboard('with filters'); await PageObjects.header.waitUntilLoadingHasFinished(); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.equal(1); + const filters = await PageObjects.dashboard.getFilters(); + expect(filters.length).to.equal(1); await pieChart.expectPieSliceCount(1); }); diff --git a/test/functional/apps/dashboard/_dashboard_filtering.js b/test/functional/apps/dashboard/_dashboard_filtering.js index ebabc4c691cc07..a127f96f8bc7cd 100644 --- a/test/functional/apps/dashboard/_dashboard_filtering.js +++ b/test/functional/apps/dashboard/_dashboard_filtering.js @@ -172,7 +172,7 @@ export default function ({ getService, getPageObjects }) { describe('disabling a filter unfilters the data on', async () => { before(async () => { - await filterBar.toggleFilterEnabled('bytes'); + await testSubjects.click('disableFilter-bytes'); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.dashboard.waitForRenderComplete(); }); diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index f5612be7a611fb..64e2a2fdc952e8 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -413,7 +413,7 @@ export default function ({ getService, getPageObjects }) { it('should show the phrases if you re-open a phrases filter', async function () { await filterBar.clickEditFilter('extension.raw', 'jpg'); - const phrases = await filterBar.getFilterEditorSelectedPhrases(); + const phrases = await filterBar.getFilterEditorPhrases(); expect(phrases.length).to.be(1); expect(phrases[0]).to.be('jpg'); }); diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index fa12727f214349..fd20b35a74666b 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -38,7 +38,6 @@ export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const inspector = getService('inspector'); const testSubjects = getService('testSubjects'); - const filterBar = getService('filterBar'); const PageObjects = getPageObjects(['common', 'header', 'settings', 'visualize', 'discover']); describe('scripted fields', () => { @@ -129,7 +128,7 @@ export default function ({ getService, getPageObjects }) { ['20', '23'], ['19', '21'], ['6', '20'], ['17', '20'], ['30', '20'], ['13', '19'], ['18', '18'], ['16', '17'], ['5', '16'], ['8', '16'], ['15', '14'], ['3', '13'], ['2', '12'], ['9', '10'], ['4', '9'] ]; - await filterBar.removeAllFilters(); + await PageObjects.discover.removeAllFilters(); await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.visualize.waitForVisualization(); @@ -186,7 +185,7 @@ export default function ({ getService, getPageObjects }) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('27'); }); - await filterBar.removeAllFilters(); + await PageObjects.discover.removeAllFilters(); }); it('should visualize scripted field in vertical bar chart', async function () { @@ -248,7 +247,7 @@ export default function ({ getService, getPageObjects }) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('359'); }); - await filterBar.removeAllFilters(); + await PageObjects.discover.removeAllFilters(); }); it('should visualize scripted field in vertical bar chart', async function () { @@ -310,7 +309,7 @@ export default function ({ getService, getPageObjects }) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('1'); }); - await filterBar.removeAllFilters(); + await PageObjects.discover.removeAllFilters(); }); it('should visualize scripted field in vertical bar chart', async function () { diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 902f016f13ee83..988fd99c386c40 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -530,6 +530,10 @@ export function DashboardPageProvider({ getService, getPageObjects }) { } } + async getFilters(timeout = defaultFindTimeout) { + return await find.allByCssSelector('.filter-bar .filter', timeout); + } + async getFilterDescriptions(timeout = defaultFindTimeout) { const filters = await find.allByCssSelector( '.filter-bar > .filter > .filter-description', diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index bc2bc7cd0d7f31..48fdfe47d92fec 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -264,6 +264,13 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); } + async removeAllFilters() { + await testSubjects.click('showFilterActions'); + await testSubjects.click('removeAllFilters'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.common.waitUntilUrlIncludes('filters:!()'); + } + async removeHeaderColumn(name) { await testSubjects.moveMouseTo(`docTableHeader-${name}`); await testSubjects.click(`docTableRemoveHeader-${name}`); diff --git a/test/functional/services/dashboard/expectations.js b/test/functional/services/dashboard/expectations.js index 72b3bbf7e8847a..2f5fe2d7a7958a 100644 --- a/test/functional/services/dashboard/expectations.js +++ b/test/functional/services/dashboard/expectations.js @@ -61,12 +61,10 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async fieldSuggestions(expectedFields) { - log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`); - const fields = await filterBar.getFilterEditorFields(); - expectedFields.forEach(expectedField => { - expect(fields).to.contain(expectedField); - }); + async fieldSuggestionIndexPatterns(expectedIndexPatterns) { + log.debug(`DashboardExpect.fieldSuggestionIndexPatterns(${expectedIndexPatterns})`); + const indexPatterns = await filterBar.getFilterFieldIndexPatterns(); + expect(indexPatterns).to.eql(expectedIndexPatterns); } async legendValuesToExist(legendValues) { diff --git a/test/functional/services/filter_bar.js b/test/functional/services/filter_bar.js index 413e46becaa0cf..18f0917814726b 100644 --- a/test/functional/services/filter_bar.js +++ b/test/functional/services/filter_bar.js @@ -18,10 +18,19 @@ */ export function FilterBarProvider({ getService, getPageObjects }) { + const browser = getService('browser'); const testSubjects = getService('testSubjects'); - const comboBox = getService('comboBox'); + const find = getService('find'); const PageObjects = getPageObjects(['common', 'header']); + async function typeIntoReactSelect(testSubj, value) { + const select = await testSubjects.find(testSubj); + const input = await select.findByClassName('ui-select-search'); + await input.type(value); + const activeSelection = await select.findByClassName('active'); + await activeSelection.click(); + } + class FilterBar { hasFilter(key, value, enabled = true) { const filterActivationState = enabled ? 'enabled' : 'disabled'; @@ -31,33 +40,23 @@ export function FilterBarProvider({ getService, getPageObjects }) { } async removeFilter(key) { - await testSubjects.click(`filter & filter-key-${key}`); - await testSubjects.click(`deleteFilter`); + const filterElement = await testSubjects.find(`filter & filter-key-${key}`); + await browser.moveMouseTo(filterElement); + await testSubjects.click(`filter & filter-key-${key} removeFilter-${key}`); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } - async removeAllFilters() { - await testSubjects.click('showFilterActions'); - await testSubjects.click('removeAllFilters'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.common.waitUntilUrlIncludes('filters:!()'); - } - async toggleFilterEnabled(key) { - await testSubjects.click(`filter & filter-key-${key}`); - await testSubjects.click(`disableFilter`); + const filterElement = await testSubjects.find(`filter & filter-key-${key}`); + await browser.moveMouseTo(filterElement); + await testSubjects.click(`filter & filter-key-${key} disableFilter-${key}`); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } async toggleFilterPinned(key) { - await testSubjects.click(`filter & filter-key-${key}`); - await testSubjects.click(`pinFilter`); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } - - async getFilterCount() { - const filters = await testSubjects.findAll('filter'); - return filters.length; + const filterElement = await testSubjects.find(`filter & filter-key-${key}`); + await browser.moveMouseTo(filterElement); + await testSubjects.click(`filter & filter-key-${key} pinFilter-${key}`); } /** @@ -80,26 +79,18 @@ export function FilterBarProvider({ getService, getPageObjects }) { */ async addFilter(field, operator, ...values) { await testSubjects.click('addFilter'); - await comboBox.set('filterFieldSuggestionList', field); - await comboBox.set('filterOperatorList', operator); + await typeIntoReactSelect('filterfieldSuggestionList', field); + await typeIntoReactSelect('filterOperatorList', operator); const params = await testSubjects.find('filterParams'); - const paramsComboBoxes = await params.findAllByCssSelector('[data-test-subj~="filterParamsComboBox"]'); const paramFields = await params.findAllByTagName('input'); for (let i = 0; i < values.length; i++) { let fieldValues = values[i]; if (!Array.isArray(fieldValues)) { fieldValues = [fieldValues]; } - - if (paramsComboBoxes && paramsComboBoxes.length > 0) { - for (let j = 0; j < fieldValues.length; j++) { - await comboBox.setElement(paramsComboBoxes[i], fieldValues[j]); - } - } - else if (paramFields && paramFields.length > 0) { - for (let j = 0; j < fieldValues.length; j++) { - await paramFields[i].type(fieldValues[j]); - } + for (let j = 0; j < fieldValues.length; j++) { + await paramFields[i].type(fieldValues[j]); + await paramFields[i].pressKeys(browser.keys.RETURN); } } await testSubjects.click('saveFilter'); @@ -107,25 +98,30 @@ export function FilterBarProvider({ getService, getPageObjects }) { } async clickEditFilter(key, value) { - await testSubjects.click(`filter & filter-key-${key} & filter-value-${value}`); - await testSubjects.click(`editFilter`); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + const pill = await testSubjects.find(`filter & filter-key-${key} & filter-value-${value}`); + await browser.moveMouseTo(pill); + await testSubjects.click('editFilter'); } - async getFilterEditorSelectedPhrases() { - return await comboBox.getComboBoxSelectedOptions('filterParamsComboBox'); + async getFilterEditorPhrases() { + const spans = await testSubjects.findAll('filterEditorPhrases'); + return await Promise.all(spans.map(el => el.getVisibleText())); } - async getFilterEditorFields() { - const optionsString = await comboBox.getOptionsList('filterFieldSuggestionList'); - return optionsString.split('\n'); + async ensureFieldEditorModalIsClosed() { + const closeFilterEditorModalButtonExists = await testSubjects.exists('filterEditorModalCloseButton'); + if (closeFilterEditorModalButtonExists) { + await testSubjects.click('filterEditorModalCloseButton'); + } } - async ensureFieldEditorModalIsClosed() { - const cancelSaveFilterModalButtonExists = await testSubjects.exists('cancelSaveFilter'); - if (cancelSaveFilterModalButtonExists) { - await testSubjects.click('cancelSaveFilter'); + async getFilterFieldIndexPatterns() { + const indexPatterns = []; + const groups = await find.allByCssSelector('.ui-select-choices-group-label'); + for (let i = 0; i < groups.length; i++) { + indexPatterns.push(await groups[i].getVisibleText()); } + return indexPatterns; } } diff --git a/x-pack/plugins/beats_management/types/kibana.d.ts b/x-pack/plugins/beats_management/types/kibana.d.ts index 07a5985f55a13c..e95dc0df93beaf 100644 --- a/x-pack/plugins/beats_management/types/kibana.d.ts +++ b/x-pack/plugins/beats_management/types/kibana.d.ts @@ -4,6 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ +declare module 'ui/index_patterns' { + export type IndexPattern = any; + + export interface StaticIndexPatternField { + name: string; + type: string; + aggregatable: boolean; + searchable: boolean; + } + + export interface StaticIndexPattern { + fields: StaticIndexPatternField[]; + title: string; + } +} + declare module 'ui/autocomplete_providers' { import { StaticIndexPattern } from 'ui/index_patterns'; diff --git a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/value.js b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/value.js index e7120ca428acb4..04cbce3281eb39 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/value.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/__tests__/value.js @@ -21,78 +21,101 @@ describe('Kuery value suggestions', function () { beforeEach(() => fetchMock.post(fetchUrlMatcher, mockValues)); afterEach(() => fetchMock.restore()); - beforeEach(() => { - config = getConfigStub(true); - indexPatterns = [indexPatternResponse]; - getSuggestions = getSuggestionsProvider({ config, indexPatterns }); - }); + describe('with config setting turned off', () => { + beforeEach(() => { + config = getConfigStub(false); + indexPatterns = [indexPatternResponse]; + getSuggestions = getSuggestionsProvider({ config, indexPatterns }); + }); - it('should return a function', function () { - expect(typeof getSuggestions).to.be('function'); - }); + it('should return a function', function () { + expect(typeof getSuggestions).to.be('function'); + }); - it('should return boolean suggestions for boolean fields', async () => { - const fieldName = 'ssl'; - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); - expect(suggestions.map(({ text }) => text)).to.eql(['true ', 'false ']); + it('should not make a request for suggestions', async () => { + const fieldName = 'machine.os.raw'; + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + expect(fetchMock.called(fetchUrlMatcher)).to.be(false); + expect(suggestions).to.eql([]); + }); }); - it('should filter boolean suggestions for boolean fields', async () => { - const fieldName = 'ssl'; - const prefix = 'fa'; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); - expect(suggestions.map(({ text }) => text)).to.eql(['false ']); - }); + describe('with config setting turned on', () => { + beforeEach(() => { + config = getConfigStub(true); + indexPatterns = [indexPatternResponse]; + getSuggestions = getSuggestionsProvider({ config, indexPatterns }); + }); - it('should not make a request for non-aggregatable fields', async () => { - const fieldName = 'non-sortable'; - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); - expect(fetchMock.called(fetchUrlMatcher)).to.be(false); - expect(suggestions).to.eql([]); - }); + it('should return a function', function () { + expect(typeof getSuggestions).to.be('function'); + }); - it('should not make a request for non-string fields', async () => { - const fieldName = 'bytes'; - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); - expect(fetchMock.called(fetchUrlMatcher)).to.be(false); - expect(suggestions).to.eql([]); - }); + it('should return boolean suggestions for boolean fields', async () => { + const fieldName = 'ssl'; + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + expect(suggestions.map(({ text }) => text)).to.eql(['true ', 'false ']); + }); - it('should make a request for string fields', async () => { - const fieldName = 'machine.os.raw'; - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); - - const lastCall = fetchMock.lastCall(fetchUrlMatcher, 'POST'); - expect(lastCall[0]).to.eql('/api/kibana/suggestions/values/logstash-*'); - expect(lastCall[1]).to.eql({ - method: 'POST', - body: '{"query":"","field":"machine.os.raw","boolFilter":[]}', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - 'kbn-version': '1.2.3', - }, + it('should filter boolean suggestions for boolean fields', async () => { + const fieldName = 'ssl'; + const prefix = 'fa'; + const suffix = ''; + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + expect(suggestions.map(({ text }) => text)).to.eql(['false ']); + }); + + it('should not make a request for non-aggregatable fields', async () => { + const fieldName = 'non-sortable'; + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + expect(fetchMock.called(fetchUrlMatcher)).to.be(false); + expect(suggestions).to.eql([]); + }); + + it('should not make a request for non-string fields', async () => { + const fieldName = 'bytes'; + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + expect(fetchMock.called(fetchUrlMatcher)).to.be(false); + expect(suggestions).to.eql([]); + }); + + it('should make a request for string fields', async () => { + const fieldName = 'machine.os.raw'; + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + + const lastCall = fetchMock.lastCall(fetchUrlMatcher, 'POST'); + expect(lastCall[0]).to.eql('/api/kibana/suggestions/values/logstash-*'); + expect(lastCall[1]).to.eql({ + method: 'POST', + body: '{"query":"","field":"machine.os.raw"}', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + 'kbn-version': '1.2.3', + }, + }); + expect(suggestions.map(({ text }) => text)).to.eql(['"foo" ', '"bar" ']); }); - expect(suggestions.map(({ text }) => text)).to.eql(['"foo" ', '"bar" ']); - }); - it('should not have descriptions', async () => { - const fieldName = 'ssl'; - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); - expect(suggestions.length).to.be.greaterThan(0); - suggestions.forEach(suggestion => { - expect(suggestion.description).to.not.be.ok(); + it('should not have descriptions', async () => { + const fieldName = 'ssl'; + const prefix = ''; + const suffix = ''; + const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + expect(suggestions.length).to.be.greaterThan(0); + suggestions.forEach(suggestion => { + expect(suggestion.description).to.not.be.ok(); + }); }); }); }); diff --git a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/field.js b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/field.js index 9ea853ab63271e..762ab51324668d 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/field.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/field.js @@ -23,12 +23,11 @@ function getDescription(fieldName) { } export function getSuggestionsProvider({ indexPatterns }) { - const allFields = flatten(indexPatterns.map(indexPattern => { - return indexPattern.fields.filter(isFilterable); - })); + const allFields = flatten(indexPatterns.map(indexPattern => indexPattern.fields)); return function getFieldSuggestions({ start, end, prefix, suffix }) { const search = `${prefix}${suffix}`.toLowerCase(); - const fieldNames = allFields.map(field => field.name); + const filterableFields = allFields.filter(isFilterable); + const fieldNames = filterableFields.map(field => field.name); const matchingFieldNames = fieldNames.filter(name => name.toLowerCase().includes(search)); const sortedFieldNames = sortPrefixFirst(matchingFieldNames.sort(keywordComparator), search); const suggestions = sortedFieldNames.map(fieldName => { diff --git a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js index 5ad41494d42165..76139e4e76728e 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js @@ -96,9 +96,7 @@ function getDescription(operator) { } export function getSuggestionsProvider({ indexPatterns }) { - const allFields = flatten(indexPatterns.map(indexPattern => { - return indexPattern.fields.slice(); - })); + const allFields = flatten(indexPatterns.map(indexPattern => indexPattern.fields)); return function getOperatorSuggestions({ end, fieldName }) { const fields = allFields.filter(field => field.name === fieldName); return flatten(fields.map(field => { 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 36fb77a30acd0e..faea99c8f0f6a4 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js @@ -4,13 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten } from 'lodash'; +import { flatten, memoize } from 'lodash'; import { escapeQuotes } from './escape_kuery'; -import { getSuggestions } from 'ui/value_suggestions'; +import { kfetch } from 'ui/kfetch'; const type = 'value'; -export function getSuggestionsProvider({ indexPatterns, boolFilter }) { +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 }) { const allFields = flatten( indexPatterns.map(indexPattern => { return indexPattern.fields.map(field => ({ @@ -19,6 +27,7 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) { })); }) ); + const shouldSuggestValues = config.get('filterEditor:suggestValues'); return function getValueSuggestions({ start, @@ -31,8 +40,18 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) { const query = `${prefix}${suffix}`; const suggestionsByField = fields.map(field => { - return getSuggestions(field.indexPatternTitle, field, query, boolFilter).then(data => { - const quotedValues = data.map(value => typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}`); + 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 => { + const quotedValues = data.map(value => `"${escapeQuotes(value)}"`); return wrapAsSuggestions(start, end, query, quotedValues); }); }); @@ -51,3 +70,9 @@ 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('|'); +} diff --git a/x-pack/plugins/ml/public/explorer/explorer.html b/x-pack/plugins/ml/public/explorer/explorer.html index 6ffd8816de30fd..9fff39b76deebc 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.html +++ b/x-pack/plugins/ml/public/explorer/explorer.html @@ -6,11 +6,9 @@ + ng-show="queryFilters.length" + state="appState"> +
diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index 32507fb10e1b15..dd3638f1727e66 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -5,6 +5,7 @@ */ + /* * Angular controller for the Machine Learning Explorer dashboard. The controller makes * multiple queries to Elasticsearch to obtain the data to populate all the components @@ -48,16 +49,13 @@ uiRoutes CheckLicense: checkFullLicense, privileges: checkGetJobsPrivilege, indexPatterns: loadIndexPatterns, - }, + } }); import { uiModules } from 'ui/modules'; -import { getFromSavedObject } from 'ui/index_patterns/static_utils'; - const module = uiModules.get('apps/ml'); module.controller('MlExplorerController', function ( - $route, $injector, $scope, $timeout, @@ -77,7 +75,6 @@ module.controller('MlExplorerController', function ( // For the moment that's the job selector and the (hidden) filter bar. $scope.jobs = []; $scope.queryFilters = []; - $scope.indexPatterns = $route.current ? $route.current.locals.indexPatterns.map(getFromSavedObject) : []; timefilter.enableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); @@ -99,11 +96,6 @@ module.controller('MlExplorerController', function ( return job; }); - $scope.updateFilters = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); - }; - const selectedJobs = jobs.filter(job => job.selected); function fieldFormatServiceCallback() { @@ -116,7 +108,7 @@ module.controller('MlExplorerController', function ( loading: false, noJobsFound, selectedCells, - selectedJobs, + selectedJobs }); } @@ -165,7 +157,7 @@ module.controller('MlExplorerController', function ( fullJobs: resp.jobs, selectedCells, selectedJobIds, - swimlaneViewByFieldName: $scope.appState.mlExplorerSwimlane.viewByFieldName, + swimlaneViewByFieldName: $scope.appState.mlExplorerSwimlane.viewByFieldName }); } else { mlExplorerDashboardService.explorer.changed(EXPLORER_ACTION.RELOAD, { @@ -179,7 +171,6 @@ module.controller('MlExplorerController', function ( }); } } - mlExplorerDashboardService.explorer.watch(loadJobsListener); // Listen for changes to job selection. @@ -206,7 +197,6 @@ module.controller('MlExplorerController', function ( // Only redraw 100ms after last resize event. resizeTimeout = $timeout(redrawOnResize, 100); } - $(window).resize(jqueryRedrawOnResize); const navListener = $scope.$on('globalNav:update', () => { diff --git a/x-pack/plugins/ml/public/util/index_utils.js b/x-pack/plugins/ml/public/util/index_utils.js index 582c1fae4f883d..9e5353ab668511 100644 --- a/x-pack/plugins/ml/public/util/index_utils.js +++ b/x-pack/plugins/ml/public/util/index_utils.js @@ -22,7 +22,7 @@ export function loadIndexPatterns(Private, indexPatterns) { const savedObjectsClient = Private(SavedObjectsClientProvider); return savedObjectsClient.find({ type: 'index-pattern', - fields: ['id', 'title', 'type', 'fields'], + fields: ['title', 'type'], perPage: 10000 }).then((response) => { indexPatternCache = response.savedObjects; diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index b1739bdf4fb020..cb701ffb6f32bf 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -16,7 +16,6 @@ export default function ({ getService, getPageObjects }) { const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); const appsMenu = getService('appsMenu'); - const filterBar = getService('filterBar'); const PageObjects = getPageObjects([ 'security', 'common', @@ -141,8 +140,8 @@ export default function ({ getService, getPageObjects }) { it('can filter on a visualization', async () => { await PageObjects.dashboard.setTimepickerInHistoricalDataRange(); await pieChart.filterOnPieSlice(); - const filterCount = await filterBar.getFilterCount(); - expect(filterCount).to.equal(1); + const filters = await PageObjects.dashboard.getFilters(); + expect(filters.length).to.equal(1); }); it('does not show the edit menu item', async () => {