diff --git a/packages/kbn-es-query/README.md b/packages/kbn-es-query/README.md new file mode 100644 index 00000000000000..53f082b864d732 --- /dev/null +++ b/packages/kbn-es-query/README.md @@ -0,0 +1,94 @@ +# 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 ce3e96cc8219ca..ec4226f6b1e758 100644 --- a/packages/kbn-es-query/package.json +++ b/packages/kbn-es-query/package.json @@ -5,14 +5,15 @@ "license": "Apache-2.0", "private": true, "scripts": { - "build": "babel src --out-dir target", + "build": "tsc && 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 dd0d62f36c1461..0c82279fb44176 100644 --- a/packages/kbn-es-query/src/filters/exists.js +++ b/packages/kbn-es-query/src/filters/exists.js @@ -17,6 +17,7 @@ * under the License. */ +// Creates a filter where the given field exists export function buildExistsFilter(field, indexPattern) { return { meta: { diff --git a/src/ui/public/filter_bar/lib/disable_filter.js b/packages/kbn-es-query/src/filters/index.d.ts similarity index 50% rename from src/ui/public/filter_bar/lib/disable_filter.js rename to packages/kbn-es-query/src/filters/index.d.ts index 815bc4eb2a83ac..c46a767e38ea4c 100644 --- a/src/ui/public/filter_bar/lib/disable_filter.js +++ b/packages/kbn-es-query/src/filters/index.d.ts @@ -17,28 +17,31 @@ * under the License. */ -export function disableFilter(filter) { - return setFilterDisabled(filter, true); -} +import { Field, IndexPattern } from 'ui/index_patterns'; +import { CustomFilter, ExistsFilter, PhraseFilter, PhrasesFilter, RangeFilter } from './lib'; +import { RangeFilterParams } from './lib/range_filter'; -export function enableFilter(filter) { - return setFilterDisabled(filter, false); -} +export * from './lib'; -export function toggleFilterDisabled(filter) { - const { meta: { disabled = false } = {} } = filter; +export function buildExistsFilter(field: Field, indexPattern: IndexPattern): ExistsFilter; - return setFilterDisabled(filter, !disabled); -} +export function buildPhraseFilter( + field: Field, + value: string, + indexPattern: IndexPattern +): PhraseFilter; -function setFilterDisabled(filter, disabled) { - const { meta = {} } = filter; +export function buildPhrasesFilter( + field: Field, + values: string[], + indexPattern: IndexPattern +): PhrasesFilter; - return { - ...filter, - meta: { - ...meta, - disabled, - } - }; -} +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 dfd89247368620..d7d092eabd8a2f 100644 --- a/packages/kbn-es-query/src/filters/index.js +++ b/packages/kbn-es-query/src/filters/index.js @@ -22,3 +22,4 @@ export * from './phrase'; export * from './phrases'; export * from './query'; export * from './range'; +export * from './lib'; diff --git a/src/ui/public/filter_bar/lib/filter_applied_and_unwrap.js b/packages/kbn-es-query/src/filters/lib/custom_filter.ts similarity index 87% rename from src/ui/public/filter_bar/lib/filter_applied_and_unwrap.js rename to packages/kbn-es-query/src/filters/lib/custom_filter.ts index 843ff6daf10494..1003cc984a90f4 100644 --- a/src/ui/public/filter_bar/lib/filter_applied_and_unwrap.js +++ b/packages/kbn-es-query/src/filters/lib/custom_filter.ts @@ -17,9 +17,8 @@ * under the License. */ -import _ from 'lodash'; - -export function filterAppliedAndUnwrap(filters) { - return _.filter(filters, 'meta.apply'); -} +import { Filter } from './meta_filter'; +export type CustomFilter = Filter & { + query: any; +}; diff --git a/packages/kbn-es-query/src/filters/lib/exists_filter.ts b/packages/kbn-es-query/src/filters/lib/exists_filter.ts new file mode 100644 index 00000000000000..356d039f4d19bd --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/exists_filter.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filter, FilterMeta } from './meta_filter'; + +export type ExistsFilterMeta = FilterMeta; + +export type ExistsFilter = Filter & { + meta: ExistsFilterMeta; +}; diff --git a/src/fixtures/filters/index.js b/packages/kbn-es-query/src/filters/lib/geo_bounding_box_filter.ts similarity index 75% rename from src/fixtures/filters/index.js rename to packages/kbn-es-query/src/filters/lib/geo_bounding_box_filter.ts index e8f823c4e8cbe4..c83e146b093a35 100644 --- a/src/fixtures/filters/index.js +++ b/packages/kbn-es-query/src/filters/lib/geo_bounding_box_filter.ts @@ -17,8 +17,15 @@ * under the License. */ -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'; +import { Filter, FilterMeta, LatLon } from './meta_filter'; + +export type GeoBoundingBoxFilterMeta = FilterMeta & { + params: { + bottom_right: LatLon; + top_left: LatLon; + }; +}; + +export type GeoBoundingBoxFilter = Filter & { + meta: GeoBoundingBoxFilterMeta; +}; diff --git a/packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts b/packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts new file mode 100644 index 00000000000000..80b606e13bb3de --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filter, FilterMeta, LatLon } from './meta_filter'; + +export type GeoPolygonFilterMeta = FilterMeta & { + params: { + points: LatLon[]; + }; +}; + +export type GeoPolygonFilter = Filter & { + meta: GeoPolygonFilterMeta; +}; diff --git a/packages/kbn-es-query/src/filters/lib/index.ts b/packages/kbn-es-query/src/filters/lib/index.ts new file mode 100644 index 00000000000000..fdf87c84eb5caf --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/index.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// 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 new file mode 100644 index 00000000000000..db5b308a01da38 --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/meta_filter.ts @@ -0,0 +1,100 @@ +/* + * 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/phrase_filter.ts b/packages/kbn-es-query/src/filters/lib/phrase_filter.ts new file mode 100644 index 00000000000000..a8613190ce786b --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/phrase_filter.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filter, FilterMeta } from './meta_filter'; + +export type PhraseFilterMeta = FilterMeta & { + params: { + query: string; // The unformatted value + }; +}; + +export type PhraseFilter = Filter & { + meta: PhraseFilterMeta; +}; diff --git a/packages/kbn-es-query/src/filters/lib/phrases_filter.ts b/packages/kbn-es-query/src/filters/lib/phrases_filter.ts new file mode 100644 index 00000000000000..cc36ad54c8dc4a --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/phrases_filter.ts @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { 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 new file mode 100644 index 00000000000000..1f6a95844437a6 --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/query_string_filter.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filter, FilterMeta } from './meta_filter'; + +export type QueryStringFilterMeta = FilterMeta; + +export type QueryStringFilter = Filter & { + meta: QueryStringFilterMeta; +}; diff --git a/packages/kbn-es-query/src/filters/lib/range_filter.ts b/packages/kbn-es-query/src/filters/lib/range_filter.ts new file mode 100644 index 00000000000000..214652fb3f3327 --- /dev/null +++ b/packages/kbn-es-query/src/filters/lib/range_filter.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Filter, FilterMeta } from './meta_filter'; + +export interface RangeFilterParams { + gt?: number | string; + gte?: number | string; + lte?: number | string; + lt?: number | string; +} + +export type RangeFilterMeta = FilterMeta & { + params: RangeFilterParams; +}; + +export type RangeFilter = Filter & { + meta: RangeFilterMeta; +}; diff --git a/packages/kbn-es-query/src/filters/phrase.js b/packages/kbn-es-query/src/filters/phrase.js index 8677fde87ceda2..b71724ab60d766 100644 --- a/packages/kbn-es-query/src/filters/phrase.js +++ b/packages/kbn-es-query/src/filters/phrase.js @@ -17,6 +17,7 @@ * 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 0d85669e65fcde..f02b3763f37bb7 100644 --- a/packages/kbn-es-query/src/filters/phrases.js +++ b/packages/kbn-es-query/src/filters/phrases.js @@ -19,6 +19,8 @@ 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 428d656364987c..cfb1a6d36d9e2f 100644 --- a/packages/kbn-es-query/src/filters/query.js +++ b/packages/kbn-es-query/src/filters/query.js @@ -17,6 +17,7 @@ * 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 ec3fc167f75d4e..88be09c9971546 100644 --- a/packages/kbn-es-query/src/filters/range.js +++ b/packages/kbn-es-query/src/filters/range.js @@ -36,6 +36,8 @@ 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 79e6903b186448..873636a28889fd 100644 --- a/packages/kbn-es-query/src/index.d.ts +++ b/packages/kbn-es-query/src/index.d.ts @@ -18,3 +18,4 @@ */ export * from './kuery'; +export * from './filters'; diff --git a/packages/kbn-es-query/tsconfig.json b/packages/kbn-es-query/tsconfig.json index 9a22ea4fc44aa3..c2b6e3075dc675 100644 --- a/packages/kbn-es-query/tsconfig.json +++ b/packages/kbn-es-query/tsconfig.json @@ -1,7 +1,11 @@ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.browser.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./target" + }, "include": [ "index.d.ts", - "src/**/*.d.ts" + "src/**/*.ts" ] } diff --git a/src/fixtures/filters/scripted_phrase_filter.js b/src/fixtures/filters/scripted_phrase_filter.js deleted file mode 100644 index f21a6356de19f0..00000000000000 --- a/src/fixtures/filters/scripted_phrase_filter.js +++ /dev/null @@ -1,42 +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 const scriptedPhraseFilter = { - 'meta': { - 'negate': false, - 'index': 'logstash-*', - 'field': 'script string', - 'type': 'phrase', - 'key': 'script string', - 'value': 'i am a string', - 'disabled': false - }, - '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 9bdd538b850bb8..ea9b66ac29e56b 100644 --- a/src/legacy/core_plugins/kibana/public/context/app.html +++ b/src/legacy/core_plugins/kibana/public/context/app.html @@ -19,7 +19,12 @@ - +
(predecessorCount) => ( @@ -65,6 +67,10 @@ 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); @@ -74,6 +80,7 @@ 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 0aa3c6eb930ccf..67774b34008168 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.html @@ -30,22 +30,25 @@
- + filters="model.filters" + on-filters-updated="onFiltersUpdated" + show-filter-bar="showFilterBar()" + watch-depth="reference" + >
- - +
0) { + $scope.indexPatterns = panelIndexPatterns; + } + else { + indexPatterns.getDefault().then((defaultIndexPattern) => { + $scope.$evalAsync(() => { + $scope.indexPatterns = [defaultIndexPattern]; + }); + }); + } }; // Part of the exposed plugin API - do not remove without careful consideration. @@ -153,7 +167,7 @@ app.directive('dashboardApp', function ($injector) { query: '', language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage') }, - filterBar.getFilters() + queryFilter.getFilters() ); timefilter.enableAutoRefreshSelector(); @@ -224,11 +238,31 @@ app.directive('dashboardApp', function ($injector) { dashboardStateManager.requestReload(); } else { $scope.model.query = migrateLegacyQuery(query); - dashboardStateManager.applyFilters($scope.model.query, filterBar.getFilters()); + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); } $scope.refresh(); }; + $scope.onFiltersUpdated = filters => { + // The filters will automatically be set when the queryFilter emits an update event (see below) + queryFilter.setFilters(filters); + }; + + $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) => { @@ -347,7 +381,7 @@ app.directive('dashboardApp', function ($injector) { }); } - $scope.showFilterBar = () => filterBar.getFilters().length > 0 || !dashboardStateManager.getFullScreenMode(); + $scope.showFilterBar = () => $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); $scope.showAddPanel = () => { dashboardStateManager.setFullScreenMode(false); @@ -458,12 +492,13 @@ app.directive('dashboardApp', function ($injector) { updateViewMode(dashboardStateManager.getViewMode()); // update root source when filters update - $scope.$listen(filterBar, 'update', function () { - dashboardStateManager.applyFilters($scope.model.query, filterBar.getFilters()); + $scope.$listen(queryFilter, 'update', function () { + $scope.model.filters = queryFilter.getFilters(); + dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters); }); // update data when filters fire fetch event - $scope.$listen(filterBar, 'fetch', $scope.refresh); + $scope.$listen(queryFilter, 'fetch', $scope.refresh); $scope.$on('$destroy', () => { dashboardStateManager.destroy(); diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index cedaa5fbb232aa..5845625163cd3b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -33,7 +33,7 @@ import 'ui/filters/moment'; import 'ui/index_patterns'; import 'ui/state_management/app_state'; import { timefilter } from 'ui/timefilter'; -import 'ui/query_bar'; +import 'ui/search_bar'; import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier'; import { toastNotifications } from 'ui/notify'; import { VisProvider } from 'ui/vis'; @@ -351,6 +351,24 @@ 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 @@ -488,6 +506,7 @@ function discoverController( // update data source when filters update $scope.$listen(queryFilter, 'update', function () { + $scope.filters = queryFilter.getFilters(); return $scope.updateDataSource().then(function () { $state.save(); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/index.html b/src/legacy/core_plugins/kibana/public/discover/index.html index f9fae245a64693..b11097a07ce023 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.html +++ b/src/legacy/core_plugins/kibana/public/discover/index.html @@ -30,23 +30,20 @@

- + filters="filters" + on-filters-updated="onFiltersUpdated" + watch-depth="reference" + >

-
- -
- + + +
{ + // 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; @@ -353,6 +376,7 @@ function VisEditor( // update the searchSource when filters update $scope.$listen(queryFilter, 'update', function () { + $scope.filters = queryFilter.getFilters(); $scope.fetch(); }); diff --git a/src/ui/public/_index.scss b/src/ui/public/_index.scss index fc8e02aadc8da7..88726b1a69e5e0 100644 --- a/src/ui/public/_index.scss +++ b/src/ui/public/_index.scss @@ -26,6 +26,7 @@ @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 47e7bebf9f3652..416e9c1b45fd2c 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 = {}; + const indexPattern = { id: '1234', fields: [], title: 'foo' }; 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 new file mode 100644 index 00000000000000..67f78b0951f191 --- /dev/null +++ b/src/ui/public/apply_filters/apply_filters_popover.tsx @@ -0,0 +1,128 @@ +/* + * 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 new file mode 100644 index 00000000000000..ed7a5d70a2b80b --- /dev/null +++ b/src/ui/public/apply_filters/directive.html @@ -0,0 +1,7 @@ + diff --git a/src/ui/public/apply_filters/directive.js b/src/ui/public/apply_filters/directive.js new file mode 100644 index 00000000000000..d364a1843494a4 --- /dev/null +++ b/src/ui/public/apply_filters/directive.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 '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/filter_bar/index.js b/src/ui/public/apply_filters/index.ts similarity index 86% rename from src/ui/public/filter_bar/index.js rename to src/ui/public/apply_filters/index.ts index 082a17b501ed97..4346316e623740 100644 --- a/src/ui/public/filter_bar/index.js +++ b/src/ui/public/apply_filters/index.ts @@ -17,6 +17,6 @@ * under the License. */ -import './filter_bar'; // directive +import './directive'; -export { disableFilter, enableFilter, toggleFilterDisabled } from './lib/disable_filter'; +export { ApplyFiltersPopover } from './apply_filters_popover'; diff --git a/src/ui/public/doc_table/components/table_row.js b/src/ui/public/doc_table/components/table_row.js index b7c5768bbaa9a2..f16be56778355d 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 '../../filter_bar'; +import { disableFilter } from '@kbn/es-query'; import { dispatchRenderComplete } from '../../render_complete'; const module = uiModules.get('app/discover'); diff --git a/src/ui/public/filter_bar/filter_pill/index.js b/src/ui/public/documentation_links/index.d.ts similarity index 94% rename from src/ui/public/filter_bar/filter_pill/index.js rename to src/ui/public/documentation_links/index.d.ts index 3db610588eb754..b37021b497de39 100644 --- a/src/ui/public/filter_bar/filter_pill/index.js +++ b/src/ui/public/documentation_links/index.d.ts @@ -17,4 +17,4 @@ * under the License. */ -import './filter_pill'; +export function getDocLink(id: string): string; diff --git a/src/ui/public/filter_bar/__tests__/filter_bar.js b/src/ui/public/filter_bar/__tests__/filter_bar.js deleted file mode 100644 index a17875a3d88376..00000000000000 --- a/src/ui/public/filter_bar/__tests__/filter_bar.js +++ /dev/null @@ -1,228 +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 _ 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 deleted file mode 100644 index 612956327c18fe..00000000000000 --- a/src/ui/public/filter_bar/__tests__/filter_bar_click_handler.js +++ /dev/null @@ -1,80 +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 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 new file mode 100644 index 00000000000000..9ed40964a95b4b --- /dev/null +++ b/src/ui/public/filter_bar/_global_filter_group.scss @@ -0,0 +1,22 @@ +// 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 new file mode 100644 index 00000000000000..b3bc510be0c746 --- /dev/null +++ b/src/ui/public/filter_bar/_global_filter_item.scss @@ -0,0 +1,37 @@ +@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 new file mode 100644 index 00000000000000..3c57b7fe2ca3ad --- /dev/null +++ b/src/ui/public/filter_bar/_index.scss @@ -0,0 +1,2 @@ +@import 'global_filter_group'; +@import 'global_filter_item'; diff --git a/src/fixtures/filters/range_filter.js b/src/ui/public/filter_bar/directive.js similarity index 71% rename from src/fixtures/filters/range_filter.js rename to src/ui/public/filter_bar/directive.js index c18b6c98a56397..afb598de5a7b1b 100644 --- a/src/fixtures/filters/range_filter.js +++ b/src/ui/public/filter_bar/directive.js @@ -17,23 +17,13 @@ * under the License. */ -export const rangeFilter = { - 'meta': { - 'index': 'logstash-*', - 'negate': false, - 'disabled': false, - 'alias': null, - 'type': 'range', - 'key': 'bytes', - 'value': '0 to 10' - }, - 'range': { - 'bytes': { - 'gte': 0, - 'lt': 10 - } - }, - '$state': { - 'store': 'appState' - } -}; +import 'ngreact'; +import { uiModules } from '../modules'; +import { FilterBar } from './filter_bar'; +import { injectI18nProvider } from '@kbn/i18n/react'; + +const app = uiModules.get('app/kibana', ['react']); + +app.directive('filterBar', reactDirective => { + return reactDirective(injectI18nProvider(FilterBar)); +}); diff --git a/src/ui/public/filter_bar/filter_bar.html b/src/ui/public/filter_bar/filter_bar.html deleted file mode 100644 index 5cd239781d192c..00000000000000 --- a/src/ui/public/filter_bar/filter_bar.html +++ /dev/null @@ -1,149 +0,0 @@ -
-
-
-
    -
  • 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 deleted file mode 100644 index 0a1648ac75d4a9..00000000000000 --- a/src/ui/public/filter_bar/filter_bar.js +++ /dev/null @@ -1,213 +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 _ 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 979ca42ea5dc1b..e69de29bb2d1d6 100644 --- a/src/ui/public/filter_bar/filter_bar.less +++ b/src/ui/public/filter_bar/filter_bar.less @@ -1,226 +0,0 @@ -// 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 new file mode 100644 index 00000000000000..551114e0bf33aa --- /dev/null +++ b/src/ui/public/filter_bar/filter_bar.tsx @@ -0,0 +1,217 @@ +/* + * 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 deleted file mode 100644 index 060be79644d381..00000000000000 --- a/src/ui/public/filter_bar/filter_bar_click_handler.js +++ /dev/null @@ -1,89 +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 _ 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 new file mode 100644 index 00000000000000..81b129d214e37c --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/generic_combo_box.tsx @@ -0,0 +1,61 @@ +/* + * 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 new file mode 100644 index 00000000000000..de301432b1b67e --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/index.tsx @@ -0,0 +1,471 @@ +/* + * 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 new file mode 100644 index 00000000000000..859f2fcc1535a8 --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -0,0 +1,314 @@ +/* + * 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 new file mode 100644 index 00000000000000..b1b456e482ac95 --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -0,0 +1,178 @@ +/* + * 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 new file mode 100644 index 00000000000000..b97f4a73901b62 --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/lib/filter_operators.ts @@ -0,0 +1,106 @@ +/* + * 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/fixtures/filters/exists_filter.js b/src/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts similarity index 73% rename from src/fixtures/filters/exists_filter.js rename to src/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts index 0c4d6138a99b71..a17f767006f3ea 100644 --- a/src/fixtures/filters/exists_filter.js +++ b/src/ui/public/filter_bar/filter_editor/lib/fixtures/exists_filter.ts @@ -17,19 +17,18 @@ * under the License. */ -export const existsFilter = { - 'meta': { - 'index': 'logstash-*', - 'negate': false, - 'disabled': false, - 'type': 'exists', - 'key': 'machine.os', - 'value': 'exists' +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, }, - 'exists': { - 'field': 'machine.os' + $state: { + store: FilterStateStore.APP_STATE, }, - '$state': { - 'store': 'appState' - } }; diff --git a/src/fixtures/filters/phrase_filter.js b/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts similarity index 80% rename from src/fixtures/filters/phrase_filter.js rename to src/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts index 22f02347d94aa6..77bb8e06c801ad 100644 --- a/src/fixtures/filters/phrase_filter.js +++ b/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrase_filter.ts @@ -17,24 +17,22 @@ * under the License. */ -export const phraseFilter = { +import { FilterStateStore, PhraseFilter } from '@kbn/es-query'; + +export const phraseFilter: PhraseFilter = { meta: { negate: false, index: 'logstash-*', type: 'phrase', key: 'machine.os', value: 'ios', - disabled: false - }, - query: { - match: { - 'machine.os': { - query: 'ios', - type: 'phrase' - } - } + disabled: false, + alias: null, + params: { + query: 'ios', + }, }, $state: { - store: 'appState' - } + store: FilterStateStore.APP_STATE, + }, }; diff --git a/src/fixtures/filters/phrases_filter.js b/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts similarity index 70% rename from src/fixtures/filters/phrases_filter.js rename to src/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts index 184f1268c9da0c..e86c3ee1318e34 100644 --- a/src/fixtures/filters/phrases_filter.js +++ b/src/ui/public/filter_bar/filter_editor/lib/fixtures/phrases_filter.ts @@ -17,37 +17,20 @@ * under the License. */ -export const phrasesFilter = { +import { FilterStateStore, PhrasesFilter } from '@kbn/es-query'; + +export const phrasesFilter: 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 - }, - query: { - bool: { - should: [ - { - match_phrase: { - 'machine.os.raw': 'win xp' - } - }, - { - match_phrase: { - 'machine.os.raw': 'osx' - } - } - ], - minimum_should_match: 1 - } + disabled: false, + alias: null, }, $state: { - store: 'appState' - } + store: FilterStateStore.APP_STATE, + }, }; diff --git a/src/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts b/src/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts new file mode 100644 index 00000000000000..f6daf9cb36f117 --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/lib/fixtures/range_filter.ts @@ -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 { 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, + }, + }, + $state: { + store: FilterStateStore.APP_STATE, + }, +}; diff --git a/src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx b/src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx new file mode 100644 index 00000000000000..75073def34281e --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { 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 new file mode 100644 index 00000000000000..06b826d681d0cd --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/phrase_value_input.tsx @@ -0,0 +1,88 @@ +/* + * 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 new file mode 100644 index 00000000000000..bef1433223e74e --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/phrases_values_input.tsx @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { 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 new file mode 100644 index 00000000000000..7343c5722f226e --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/range_value_input.tsx @@ -0,0 +1,121 @@ +/* + * 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 new file mode 100644 index 00000000000000..0a573c88eae703 --- /dev/null +++ b/src/ui/public/filter_bar/filter_editor/value_input_type.tsx @@ -0,0 +1,120 @@ +/* + * 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 new file mode 100644 index 00000000000000..82ed8c692d5497 --- /dev/null +++ b/src/ui/public/filter_bar/filter_item.tsx @@ -0,0 +1,226 @@ +/* + * 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 deleted file mode 100644 index f79d42665e8fc0..00000000000000 --- a/src/ui/public/filter_bar/filter_pill/filter_pill.html +++ /dev/null @@ -1,97 +0,0 @@ -
- -
- - 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 deleted file mode 100644 index 03b19427af9300..00000000000000 --- a/src/ui/public/filter_bar/filter_pill/filter_pill.js +++ /dev/null @@ -1,57 +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 _ 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/filter_bar/filter_view/index.tsx b/src/ui/public/filter_bar/filter_view/index.tsx new file mode 100644 index 00000000000000..2421e7dcfab97e --- /dev/null +++ b/src/ui/public/filter_bar/filter_view/index.tsx @@ -0,0 +1,103 @@ +/* + * 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/filter_bar/index.ts b/src/ui/public/filter_bar/index.ts new file mode 100644 index 00000000000000..cdf49a72e9554e --- /dev/null +++ b/src/ui/public/filter_bar/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './directive'; + +export { FilterBar } from './filter_bar'; diff --git a/src/ui/public/filter_bar/lib/__tests__/disable_filter.js b/src/ui/public/filter_bar/lib/__tests__/disable_filter.js deleted file mode 100644 index 2541ec6cacae08..00000000000000 --- a/src/ui/public/filter_bar/lib/__tests__/disable_filter.js +++ /dev/null @@ -1,140 +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 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/src/ui/public/filter_bar/lib/__tests__/filter_applied_and_unwrap.js b/src/ui/public/filter_bar/lib/__tests__/filter_applied_and_unwrap.js deleted file mode 100644 index 819035452837e3..00000000000000 --- a/src/ui/public/filter_bar/lib/__tests__/filter_applied_and_unwrap.js +++ /dev/null @@ -1,38 +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 expect from 'expect.js'; -import { filterAppliedAndUnwrap } from '../filter_applied_and_unwrap'; - -describe('Filter Bar Directive', function () { - describe('filterAppliedAndUnwrap()', function () { - - 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 deleted file mode 100644 index 009683e3c07f44..00000000000000 --- a/src/ui/public/filter_bar/lib/__tests__/filter_out_time_based_filter.js +++ /dev/null @@ -1,57 +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 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/filter_out_time_based_filter.js b/src/ui/public/filter_bar/lib/filter_out_time_based_filter.js deleted file mode 100644 index 0aa628e6ad069d..00000000000000 --- a/src/ui/public/filter_bar/lib/filter_out_time_based_filter.js +++ /dev/null @@ -1,33 +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 _ from 'lodash'; - -export function FilterBarLibFilterOutTimeBasedFilterProvider(indexPatterns, Promise) { - return Promise.method(function (filters) { - const id = _.get(filters, '[0].meta.index'); - if (id == null) return; - - 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 25018d4746e2bc..687c844f7eec78 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 params = isScriptedPhraseFilter ? filter.script.script.params : filter.query.match[key]; - const query = isScriptedPhraseFilter ? params.value : params.query; + const query = isScriptedPhraseFilter ? filter.script.script.params.value : filter.query.match[key].query; + const 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 params = _.get(filter, ['script', 'script', 'params']); - return params && params.value; + const value = _.get(filter, ['script', 'script', 'params', 'value']); + return typeof value !== 'undefined'; } diff --git a/src/ui/public/filter_bar/query_filter.js b/src/ui/public/filter_bar/query_filter.js index 0c1e68c084535e..5b73013973fb81 100644 --- a/src/ui/public/filter_bar/query_filter.js +++ b/src/ui/public/filter_bar/query_filter.js @@ -24,10 +24,13 @@ 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(); @@ -216,6 +219,24 @@ 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; @@ -268,7 +289,6 @@ 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) { @@ -293,8 +313,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, { disabled: true }).reverse(), - uniqFilters(appFilters, { disabled: true }).reverse() + uniqFilters(globalFilters).reverse(), + uniqFilters(appFilters).reverse() ]; } @@ -332,8 +352,7 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g // reconcile filter in global and app states const filters = mergeStateFilters(next[0], next[1]); - const globalFilters = filters[0]; - const appFilters = filters[1]; + const [globalFilters, appFilters] = filters; 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 deleted file mode 100644 index 0b69a1d5661d8a..00000000000000 --- a/src/ui/public/filter_editor/filter_editor.html +++ /dev/null @@ -1,148 +0,0 @@ -
-
-
- 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 deleted file mode 100644 index ac55a253b594fd..00000000000000 --- a/src/ui/public/filter_editor/filter_editor.js +++ /dev/null @@ -1,161 +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 _ 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 deleted file mode 100644 index b45c0030b37d39..00000000000000 --- a/src/ui/public/filter_editor/filter_editor.less +++ /dev/null @@ -1,36 +0,0 @@ -.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 deleted file mode 100644 index 65c45b1bae7b05..00000000000000 --- a/src/ui/public/filter_editor/filter_field_select.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - {{$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 deleted file mode 100644 index f1aeb276d58d1d..00000000000000 --- a/src/ui/public/filter_editor/filter_field_select.js +++ /dev/null @@ -1,52 +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 '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 deleted file mode 100644 index 78b730b93b58aa..00000000000000 --- a/src/ui/public/filter_editor/filter_operator_select.html +++ /dev/null @@ -1,12 +0,0 @@ - - - {{$select.selected.name}} - - -
-
-
diff --git a/src/ui/public/filter_editor/filter_operator_select.js b/src/ui/public/filter_editor/filter_operator_select.js deleted file mode 100644 index 3d877aee869b7a..00000000000000 --- a/src/ui/public/filter_editor/filter_operator_select.js +++ /dev/null @@ -1,42 +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 'angular-ui-select'; -import { uiModules } from '../modules'; -import { getOperatorOptions } from './lib/filter_editor_utils'; -import template from './filter_operator_select.html'; -import '../directives/ui_select_focus_on'; - -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 deleted file mode 100644 index 999dae7c1df0cf..00000000000000 --- a/src/ui/public/filter_editor/filter_query_dsl_editor.html +++ /dev/null @@ -1,13 +0,0 @@ -
diff --git a/src/ui/public/filter_editor/filter_query_dsl_editor.js b/src/ui/public/filter_editor/filter_query_dsl_editor.js deleted file mode 100644 index 16f763378ae05c..00000000000000 --- a/src/ui/public/filter_editor/filter_query_dsl_editor.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 '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/filter_editor/lib/__tests__/filter_editor_utils.js b/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js deleted file mode 100644 index 61aadc4c074873..00000000000000 --- a/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js +++ /dev/null @@ -1,396 +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 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 deleted file mode 100644 index 4c0067b62193dc..00000000000000 --- a/src/ui/public/filter_editor/lib/filter_editor_utils.js +++ /dev/null @@ -1,111 +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 _ 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 deleted file mode 100644 index 140928eb1912d8..00000000000000 --- a/src/ui/public/filter_editor/lib/filter_operators.js +++ /dev/null @@ -1,72 +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 _ 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 deleted file mode 100644 index f242ce2af5a2f6..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_editor.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - diff --git a/src/ui/public/filter_editor/params_editor/filter_params_editor.js b/src/ui/public/filter_editor/params_editor/filter_params_editor.js deleted file mode 100644 index d0397d4f7508e3..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_editor.js +++ /dev/null @@ -1,37 +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 { 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'; - -const module = uiModules.get('kibana'); -module.directive('filterParamsEditor', function () { - return { - restrict: 'E', - template, - scope: { - field: '=', - operator: '=', - params: '=' - } - }; -}); 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 deleted file mode 100644 index 1d1a89bbb0df98..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_input_type.html +++ /dev/null @@ -1,50 +0,0 @@ -
- - - - -
- -
-
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 deleted file mode 100644 index 1278e8b508be3d..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_input_type.js +++ /dev/null @@ -1,49 +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 { 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 deleted file mode 100644 index cbcd2d8ab5223a..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js +++ /dev/null @@ -1,57 +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 _ 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 deleted file mode 100644 index 36d509f28decef..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - {{$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 deleted file mode 100644 index d7b166e7990f74..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js +++ /dev/null @@ -1,42 +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 '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 deleted file mode 100644 index 4a2935c4ba0b71..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - {{$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 deleted file mode 100644 index ca4f67b28caf57..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js +++ /dev/null @@ -1,39 +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 '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 deleted file mode 100644 index 483d6c7408bd85..00000000000000 --- a/src/ui/public/filter_editor/params_editor/filter_params_range_editor.html +++ /dev/null @@ -1,30 +0,0 @@ -
- -
- -
- -
- - - - Accepted date formats - - diff --git a/src/ui/public/index_patterns/_field.d.ts b/src/ui/public/index_patterns/_field.d.ts new file mode 100644 index 00000000000000..749cd63d0a84cd --- /dev/null +++ b/src/ui/public/index_patterns/_field.d.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +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 0ff899839c42cc..0465e35ff2a1fe 100644 --- a/src/ui/public/index_patterns/_index_pattern.d.ts +++ b/src/ui/public/index_patterns/_index_pattern.d.ts @@ -17,11 +17,18 @@ * under the License. */ +import { Field } from 'ui/index_patterns/_field'; + /** * WARNING: these types are incomplete */ -export type IndexPattern = any; +export interface IndexPattern { + id: string; + fields: Field[]; + title: string; + timeFieldName?: string; +} 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 new file mode 100644 index 00000000000000..a84ab381b52d8b --- /dev/null +++ b/src/ui/public/index_patterns/fixtures/index.ts @@ -0,0 +1,79 @@ +/* + * 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 92f04543c237e2..e2d7ddd8c5254c 100644 --- a/src/ui/public/index_patterns/index.d.ts +++ b/src/ui/public/index_patterns/index.d.ts @@ -17,4 +17,9 @@ * under the License. */ -export { IndexPattern, StaticIndexPattern } from 'ui/index_patterns/_index_pattern'; +export { + IndexPattern, + StaticIndexPattern, + StaticIndexPatternField, +} from 'ui/index_patterns/_index_pattern'; +export { Field } from 'ui/index_patterns/_field'; 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 4a81b35e96555a..e825df5d294cad 100644 --- a/src/ui/public/index_patterns/static_utils/__tests__/index.js +++ b/src/ui/public/index_patterns/static_utils/__tests__/index.js @@ -20,26 +20,50 @@ import expect from 'expect.js'; import { isFilterable } from '../index'; +const mockField = { + name: 'foo', + scripted: false, + searchable: true, + type: 'string', +}; + describe('static utils', () => { describe('isFilterable', () => { - it('should be filterable', () => { - ['string', 'number', 'date', 'ip', 'boolean'].forEach(type => { - expect(isFilterable({ type })).to.be(true); + 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 not be filterable', () => { - [ - 'geo_point', - 'geo_shape', - 'attachment', - 'murmur3', - '_source', - 'unknown', - 'conflict', - ].forEach(type => { - expect(isFilterable({ type })).to.be(false); + 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 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); }); }); }); 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 6d387bb95882f5..c3e02f2b20d79a 100644 --- a/src/ui/public/index_patterns/static_utils/index.d.ts +++ b/src/ui/public/index_patterns/static_utils/index.d.ts @@ -17,13 +17,6 @@ * under the License. */ -import { StaticIndexPattern } from 'ui/index_patterns'; +import { Field } from 'ui/index_patterns'; -interface SavedObject { - attributes: { - fields: string; - title: string; - }; -} - -export function getFromLegacyIndexPattern(indexPatterns: any[]): StaticIndexPattern[]; +export function isFilterable(field: Field): boolean; diff --git a/src/ui/public/index_patterns/static_utils/index.js b/src/ui/public/index_patterns/static_utils/index.js index 2cf43c319b10ae..2285858c40cccd 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 filterableTypes.includes(field.type); + return field.name === '_id' || field.scripted || (field.searchable && filterableTypes.includes(field.type)); } export function getFromSavedObject(savedObject) { @@ -31,14 +31,8 @@ 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 fb092e9f684c01..e3465e4b50c514 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,7 +34,6 @@ exports[`QueryBar Should disable autoFocus on EuiFieldText when disableAutoFocus role="form" >
({ }); const mockIndexPattern = { + id: '1234', title: 'logstash-*', - fields: { - raw: [ - { - name: 'response', - type: 'number', - aggregatable: true, - searchable: true, - }, - ], - }, + fields: [ + { + name: 'response', + type: 'number', + aggregatable: true, + filterable: 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 af91fea37eb437..401950f8adf273 100644 --- a/src/ui/public/query_bar/components/query_bar.tsx +++ b/src/ui/public/query_bar/components/query_bar.tsx @@ -21,7 +21,6 @@ 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'; @@ -74,6 +73,7 @@ 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 = getFromLegacyIndexPattern(this.props.indexPatterns); + const indexPatterns = 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/filter_editor/index.js b/src/ui/public/search_bar/components/index.tsx similarity index 95% rename from src/ui/public/filter_editor/index.js rename to src/ui/public/search_bar/components/index.tsx index dc613de885ee7e..131c6059934a47 100644 --- a/src/ui/public/filter_editor/index.js +++ b/src/ui/public/search_bar/components/index.tsx @@ -17,4 +17,4 @@ * under the License. */ -import './filter_editor'; +export { SearchBar } from './search_bar'; diff --git a/src/ui/public/search_bar/components/search_bar.tsx b/src/ui/public/search_bar/components/search_bar.tsx new file mode 100644 index 00000000000000..ac7d5399e71446 --- /dev/null +++ b/src/ui/public/search_bar/components/search_bar.tsx @@ -0,0 +1,178 @@ +/* + * 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/filter_editor/params_editor/filter_params_range_editor.js b/src/ui/public/search_bar/directive/index.js similarity index 69% rename from src/ui/public/filter_editor/params_editor/filter_params_range_editor.js rename to src/ui/public/search_bar/directive/index.js index 8bab49a1bbe0a6..f03624dd189a23 100644 --- a/src/ui/public/filter_editor/params_editor/filter_params_range_editor.js +++ b/src/ui/public/search_bar/directive/index.js @@ -17,20 +17,20 @@ * under the License. */ +import 'ngreact'; import { uiModules } from '../../modules'; -import template from './filter_params_range_editor.html'; -import './filter_params_input_type'; -import '../../directives/documentation_href'; -import '../../directives/focus_on'; +import { SearchBar } from '../components'; +import { injectI18nProvider } from '@kbn/i18n/react'; -const module = uiModules.get('kibana'); -module.directive('filterParamsRangeEditor', function () { - return { - restrict: 'E', - template, - scope: { - field: '=', - params: '=' +const app = uiModules.get('app/kibana', ['react']); + +app.directive('searchBar', (reactDirective, localStorage) => { + return reactDirective( + injectI18nProvider(SearchBar), + undefined, + {}, + { + store: localStorage, } - }; + ); }); diff --git a/src/ui/public/search_bar/index.tsx b/src/ui/public/search_bar/index.tsx new file mode 100644 index 00000000000000..2469f62781f972 --- /dev/null +++ b/src/ui/public/search_bar/index.tsx @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import './directive'; + +export { SearchBar } from './components'; diff --git a/src/ui/public/styles/bootstrap_dark.less b/src/ui/public/styles/bootstrap_dark.less index 51273dcc0e83b5..0e1dd7b017825f 100644 --- a/src/ui/public/styles/bootstrap_dark.less +++ b/src/ui/public/styles/bootstrap_dark.less @@ -4,5 +4,4 @@ @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 4baff33e7e661f..8dd3545c85a24c 100644 --- a/src/ui/public/timefilter/get_time.ts +++ b/src/ui/public/timefilter/get_time.ts @@ -18,8 +18,7 @@ */ import dateMath from '@elastic/datemath'; -import { find } from 'lodash'; -import { IndexPattern } from 'ui/index_patterns'; +import { Field, IndexPattern } from 'ui/index_patterns'; interface CalculateBoundsOptions { forceNow?: Date; @@ -58,8 +57,9 @@ export function getTime( } let filter: Filter; - const timefield: { name: string } | undefined = - indexPattern.timeFieldName && find(indexPattern.fields, { name: indexPattern.timeFieldName }); + const timefield: Field | undefined = indexPattern.fields.find( + field => field.name === indexPattern.timeFieldName + ); if (!timefield) { return; diff --git a/src/ui/public/value_suggestions/index.ts b/src/ui/public/value_suggestions/index.ts new file mode 100644 index 00000000000000..87ff473eaeece3 --- /dev/null +++ b/src/ui/public/value_suggestions/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 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 new file mode 100644 index 00000000000000..e918a8669bfe7d --- /dev/null +++ b/src/ui/public/value_suggestions/value_suggestions.test.ts @@ -0,0 +1,131 @@ +/* + * 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 new file mode 100644 index 00000000000000..31e42e9945ede0 --- /dev/null +++ b/src/ui/public/value_suggestions/value_suggestions.ts @@ -0,0 +1,53 @@ +/* + * 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 3ba4c78b0abb5c..94eb453ff54ed3 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,6 +22,19 @@ 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(); @@ -32,7 +45,6 @@ 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); @@ -46,7 +58,6 @@ 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); @@ -59,7 +70,7 @@ describe('EditorConfigProvider', () => { } function getOutputConfig(reg: EditorConfigProviderRegistry) { - return reg.getConfigForAgg({}, {}, {}).singleParam; + return reg.getConfigForAgg({}, indexPattern, {}).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 bd074d896329da..a393a949377e1c 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.fieldSuggestionIndexPatterns(['logstash-*']); + await dashboardExpect.fieldSuggestions(['bytes']); }); 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.fieldSuggestionIndexPatterns(['animals-*']); + await dashboardExpect.fieldSuggestions(['animal']); }); 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.fieldSuggestionIndexPatterns(['animals-*']); + await dashboardExpect.fieldSuggestions(['animal']); }); }); @@ -86,16 +86,16 @@ export default function ({ getService, getPageObjects }) { }); it('are not selected by default', async function () { - const filters = await PageObjects.dashboard.getFilters(1000); - expect(filters.length).to.equal(0); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).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 filters = await PageObjects.dashboard.getFilters(); - expect(filters.length).to.equal(1); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).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 filters = await PageObjects.dashboard.getFilters(); - expect(filters.length).to.equal(1); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).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 filters = await PageObjects.dashboard.getFilters(); - expect(filters.length).to.equal(1); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).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 a127f96f8bc7cd..ebabc4c691cc07 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 testSubjects.click('disableFilter-bytes'); + await filterBar.toggleFilterEnabled('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 64e2a2fdc952e8..f5612be7a611fb 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.getFilterEditorPhrases(); + const phrases = await filterBar.getFilterEditorSelectedPhrases(); 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 fd20b35a74666b..fa12727f214349 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -38,6 +38,7 @@ 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', () => { @@ -128,7 +129,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 PageObjects.discover.removeAllFilters(); + await filterBar.removeAllFilters(); await PageObjects.discover.clickFieldListItemVisualize(scriptedPainlessFieldName); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.visualize.waitForVisualization(); @@ -185,7 +186,7 @@ export default function ({ getService, getPageObjects }) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('27'); }); - await PageObjects.discover.removeAllFilters(); + await filterBar.removeAllFilters(); }); it('should visualize scripted field in vertical bar chart', async function () { @@ -247,7 +248,7 @@ export default function ({ getService, getPageObjects }) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('359'); }); - await PageObjects.discover.removeAllFilters(); + await filterBar.removeAllFilters(); }); it('should visualize scripted field in vertical bar chart', async function () { @@ -309,7 +310,7 @@ export default function ({ getService, getPageObjects }) { await retry.try(async function () { expect(await PageObjects.discover.getHitCount()).to.be('1'); }); - await PageObjects.discover.removeAllFilters(); + await filterBar.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 988fd99c386c40..902f016f13ee83 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -530,10 +530,6 @@ 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 48fdfe47d92fec..bc2bc7cd0d7f31 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -264,13 +264,6 @@ 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 2f5fe2d7a7958a..72b3bbf7e8847a 100644 --- a/test/functional/services/dashboard/expectations.js +++ b/test/functional/services/dashboard/expectations.js @@ -61,10 +61,12 @@ export function DashboardExpectProvider({ getService, getPageObjects }) { }); } - async fieldSuggestionIndexPatterns(expectedIndexPatterns) { - log.debug(`DashboardExpect.fieldSuggestionIndexPatterns(${expectedIndexPatterns})`); - const indexPatterns = await filterBar.getFilterFieldIndexPatterns(); - expect(indexPatterns).to.eql(expectedIndexPatterns); + async fieldSuggestions(expectedFields) { + log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`); + const fields = await filterBar.getFilterEditorFields(); + expectedFields.forEach(expectedField => { + expect(fields).to.contain(expectedField); + }); } async legendValuesToExist(legendValues) { diff --git a/test/functional/services/filter_bar.js b/test/functional/services/filter_bar.js index 18f0917814726b..413e46becaa0cf 100644 --- a/test/functional/services/filter_bar.js +++ b/test/functional/services/filter_bar.js @@ -18,19 +18,10 @@ */ export function FilterBarProvider({ getService, getPageObjects }) { - const browser = getService('browser'); const testSubjects = getService('testSubjects'); - const find = getService('find'); + const comboBox = getService('comboBox'); 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'; @@ -40,23 +31,33 @@ export function FilterBarProvider({ getService, getPageObjects }) { } async removeFilter(key) { - const filterElement = await testSubjects.find(`filter & filter-key-${key}`); - await browser.moveMouseTo(filterElement); - await testSubjects.click(`filter & filter-key-${key} removeFilter-${key}`); + await testSubjects.click(`filter & filter-key-${key}`); + await testSubjects.click(`deleteFilter`); 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) { - const filterElement = await testSubjects.find(`filter & filter-key-${key}`); - await browser.moveMouseTo(filterElement); - await testSubjects.click(`filter & filter-key-${key} disableFilter-${key}`); + await testSubjects.click(`filter & filter-key-${key}`); + await testSubjects.click(`disableFilter`); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } async toggleFilterPinned(key) { - const filterElement = await testSubjects.find(`filter & filter-key-${key}`); - await browser.moveMouseTo(filterElement); - await testSubjects.click(`filter & filter-key-${key} pinFilter-${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; } /** @@ -79,18 +80,26 @@ export function FilterBarProvider({ getService, getPageObjects }) { */ async addFilter(field, operator, ...values) { await testSubjects.click('addFilter'); - await typeIntoReactSelect('filterfieldSuggestionList', field); - await typeIntoReactSelect('filterOperatorList', operator); + await comboBox.set('filterFieldSuggestionList', field); + await comboBox.set('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]; } - for (let j = 0; j < fieldValues.length; j++) { - await paramFields[i].type(fieldValues[j]); - await paramFields[i].pressKeys(browser.keys.RETURN); + + 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]); + } } } await testSubjects.click('saveFilter'); @@ -98,30 +107,25 @@ export function FilterBarProvider({ getService, getPageObjects }) { } async clickEditFilter(key, value) { - const pill = await testSubjects.find(`filter & filter-key-${key} & filter-value-${value}`); - await browser.moveMouseTo(pill); - await testSubjects.click('editFilter'); + await testSubjects.click(`filter & filter-key-${key} & filter-value-${value}`); + await testSubjects.click(`editFilter`); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } - async getFilterEditorPhrases() { - const spans = await testSubjects.findAll('filterEditorPhrases'); - return await Promise.all(spans.map(el => el.getVisibleText())); + async getFilterEditorSelectedPhrases() { + return await comboBox.getComboBoxSelectedOptions('filterParamsComboBox'); } - async ensureFieldEditorModalIsClosed() { - const closeFilterEditorModalButtonExists = await testSubjects.exists('filterEditorModalCloseButton'); - if (closeFilterEditorModalButtonExists) { - await testSubjects.click('filterEditorModalCloseButton'); - } + async getFilterEditorFields() { + const optionsString = await comboBox.getOptionsList('filterFieldSuggestionList'); + return optionsString.split('\n'); } - 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()); + async ensureFieldEditorModalIsClosed() { + const cancelSaveFilterModalButtonExists = await testSubjects.exists('cancelSaveFilter'); + if (cancelSaveFilterModalButtonExists) { + await testSubjects.click('cancelSaveFilter'); } - 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 e95dc0df93beaf..07a5985f55a13c 100644 --- a/x-pack/plugins/beats_management/types/kibana.d.ts +++ b/x-pack/plugins/beats_management/types/kibana.d.ts @@ -4,22 +4,6 @@ * 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 04cbce3281eb39..e7120ca428acb4 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,101 +21,78 @@ describe('Kuery value suggestions', function () { beforeEach(() => fetchMock.post(fetchUrlMatcher, mockValues)); afterEach(() => fetchMock.restore()); - 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 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([]); - }); + beforeEach(() => { + config = getConfigStub(true); + indexPatterns = [indexPatternResponse]; + getSuggestions = getSuggestionsProvider({ config, indexPatterns }); }); - describe('with config setting turned on', () => { - beforeEach(() => { - config = getConfigStub(true); - indexPatterns = [indexPatternResponse]; - getSuggestions = getSuggestionsProvider({ config, indexPatterns }); - }); - - 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 return a function', function () { + expect(typeof getSuggestions).to.be('function'); + }); - 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 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 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 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-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 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 make a request for string fields', async () => { - const fieldName = 'machine.os.raw'; - const prefix = ''; - const suffix = ''; - const suggestions = await getSuggestions({ fieldName, prefix, suffix }); + 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([]); + }); - 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" ']); + 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', + }, }); + 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 762ab51324668d..9ea853ab63271e 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/field.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/field.js @@ -23,11 +23,12 @@ function getDescription(fieldName) { } export function getSuggestionsProvider({ indexPatterns }) { - const allFields = flatten(indexPatterns.map(indexPattern => indexPattern.fields)); + const allFields = flatten(indexPatterns.map(indexPattern => { + return indexPattern.fields.filter(isFilterable); + })); return function getFieldSuggestions({ start, end, prefix, suffix }) { const search = `${prefix}${suffix}`.toLowerCase(); - const filterableFields = allFields.filter(isFilterable); - const fieldNames = filterableFields.map(field => field.name); + const fieldNames = allFields.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 76139e4e76728e..5ad41494d42165 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/operator.js @@ -96,7 +96,9 @@ function getDescription(operator) { } export function getSuggestionsProvider({ indexPatterns }) { - const allFields = flatten(indexPatterns.map(indexPattern => indexPattern.fields)); + const allFields = flatten(indexPatterns.map(indexPattern => { + return indexPattern.fields.slice(); + })); 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 faea99c8f0f6a4..36fb77a30acd0e 100644 --- a/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js +++ b/x-pack/plugins/kuery_autocomplete/public/autocomplete_providers/value.js @@ -4,21 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten, memoize } from 'lodash'; +import { flatten } from 'lodash'; import { escapeQuotes } from './escape_kuery'; -import { kfetch } from 'ui/kfetch'; +import { getSuggestions } from 'ui/value_suggestions'; const type = 'value'; -const requestSuggestions = memoize((query, field, boolFilter) => { - return kfetch({ - pathname: `/api/kibana/suggestions/values/${field.indexPatternTitle}`, - method: 'POST', - body: JSON.stringify({ query, field: field.name, boolFilter }), - }); -}, resolver); - -export function getSuggestionsProvider({ config, indexPatterns, boolFilter }) { +export function getSuggestionsProvider({ indexPatterns, boolFilter }) { const allFields = flatten( indexPatterns.map(indexPattern => { return indexPattern.fields.map(field => ({ @@ -27,7 +19,6 @@ export function getSuggestionsProvider({ config, indexPatterns, boolFilter }) { })); }) ); - const shouldSuggestValues = config.get('filterEditor:suggestValues'); return function getValueSuggestions({ start, @@ -40,18 +31,8 @@ export function getSuggestionsProvider({ config, indexPatterns, boolFilter }) { const query = `${prefix}${suffix}`; const suggestionsByField = fields.map(field => { - if (field.type === 'boolean') { - return wrapAsSuggestions(start, end, query, ['true', 'false']); - } else if ( - !shouldSuggestValues || - !field.aggregatable || - field.type !== 'string' - ) { - return []; - } - - return requestSuggestions(query, field, boolFilter).then(data => { - const quotedValues = data.map(value => `"${escapeQuotes(value)}"`); + return getSuggestions(field.indexPatternTitle, field, query, boolFilter).then(data => { + const quotedValues = data.map(value => typeof value === 'string' ? `"${escapeQuotes(value)}"` : `${value}`); return wrapAsSuggestions(start, end, query, quotedValues); }); }); @@ -70,9 +51,3 @@ function wrapAsSuggestions(start, end, query, values) { return { type, text, start, end }; }); } - -function resolver(query, field, boolFilter) { - // Only cache results for a minute - const ttl = Math.floor(Date.now() / 1000 / 60); - return [ttl, query, field.indexPatternTitle, field.name, JSON.stringify(boolFilter)].join('|'); -} diff --git a/x-pack/plugins/ml/public/explorer/explorer.html b/x-pack/plugins/ml/public/explorer/explorer.html index 9fff39b76deebc..6ffd8816de30fd 100644 --- a/x-pack/plugins/ml/public/explorer/explorer.html +++ b/x-pack/plugins/ml/public/explorer/explorer.html @@ -6,9 +6,11 @@ - + class-name="'globalFilterGroup__filterBar'" + filters="queryFilters" + on-filters-updated="updateFilters" + index-patterns="indexPatterns" + >
diff --git a/x-pack/plugins/ml/public/explorer/explorer_controller.js b/x-pack/plugins/ml/public/explorer/explorer_controller.js index dd3638f1727e66..32507fb10e1b15 100644 --- a/x-pack/plugins/ml/public/explorer/explorer_controller.js +++ b/x-pack/plugins/ml/public/explorer/explorer_controller.js @@ -5,7 +5,6 @@ */ - /* * Angular controller for the Machine Learning Explorer dashboard. The controller makes * multiple queries to Elasticsearch to obtain the data to populate all the components @@ -49,13 +48,16 @@ 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, @@ -75,6 +77,7 @@ 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(); @@ -96,6 +99,11 @@ 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() { @@ -108,7 +116,7 @@ module.controller('MlExplorerController', function ( loading: false, noJobsFound, selectedCells, - selectedJobs + selectedJobs, }); } @@ -157,7 +165,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, { @@ -171,6 +179,7 @@ module.controller('MlExplorerController', function ( }); } } + mlExplorerDashboardService.explorer.watch(loadJobsListener); // Listen for changes to job selection. @@ -197,6 +206,7 @@ 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 9e5353ab668511..582c1fae4f883d 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: ['title', 'type'], + fields: ['id', 'title', 'type', 'fields'], 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 cb701ffb6f32bf..b1739bdf4fb020 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,6 +16,7 @@ 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', @@ -140,8 +141,8 @@ export default function ({ getService, getPageObjects }) { it('can filter on a visualization', async () => { await PageObjects.dashboard.setTimepickerInHistoricalDataRange(); await pieChart.filterOnPieSlice(); - const filters = await PageObjects.dashboard.getFilters(); - expect(filters.length).to.equal(1); + const filterCount = await filterBar.getFilterCount(); + expect(filterCount).to.equal(1); }); it('does not show the edit menu item', async () => {