mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Re-introduce new filter bar (#29752)
Fixes the build issues introduced in #25563 and re-introduces the new react/eui/typescript filter bar, essentially reverting the revert in #29662. I did have to resolve one merge conflict in query_bar.tsx, and re-deleted all of the old filter bar code where translation code had been added.
This commit is contained in:
parent
f67e89b190
commit
8a83955650
139 changed files with 4299 additions and 3582 deletions
94
packages/kbn-es-query/README.md
Normal file
94
packages/kbn-es-query/README.md
Normal file
|
@ -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.
|
||||
|
|
@ -1,20 +1,26 @@
|
|||
{
|
||||
"name": "@kbn/es-query",
|
||||
"main": "target/index.js",
|
||||
"main": "target/server/index.js",
|
||||
"browser": "target/public/index.js",
|
||||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir target",
|
||||
"kbn:bootstrap": "yarn build --quiet",
|
||||
"kbn:watch": "yarn build --watch"
|
||||
"build": "node scripts/build",
|
||||
"kbn:bootstrap": "node scripts/build --source-maps",
|
||||
"kbn:watch": "node scripts/build --source-maps --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "npm:@elastic/lodash@3.10.1-kibana1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^3.0.3",
|
||||
"@kbn/babel-preset": "1.0.0",
|
||||
"@kbn/dev-utils": "1.0.0",
|
||||
"babel-cli": "^6.26.0",
|
||||
"expect.js": "0.3.1"
|
||||
"expect.js": "0.3.1",
|
||||
"del": "^3.0.0",
|
||||
"getopts": "^2.2.3",
|
||||
"supports-color": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import './filter_editor';
|
||||
require('../tasks/build_cli');
|
|
@ -17,6 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
// Creates a filter where the given field exists
|
||||
export function buildExistsFilter(field, indexPattern) {
|
||||
return {
|
||||
meta: {
|
||||
|
|
|
@ -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;
|
|
@ -22,3 +22,4 @@ export * from './phrase';
|
|||
export * from './phrases';
|
||||
export * from './query';
|
||||
export * from './range';
|
||||
export * from './lib';
|
||||
|
|
|
@ -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;
|
||||
};
|
26
packages/kbn-es-query/src/filters/lib/exists_filter.ts
Normal file
26
packages/kbn-es-query/src/filters/lib/exists_filter.ts
Normal file
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
30
packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts
Normal file
30
packages/kbn-es-query/src/filters/lib/geo_polygon_filter.ts
Normal file
|
@ -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;
|
||||
};
|
50
packages/kbn-es-query/src/filters/lib/index.ts
Normal file
50
packages/kbn-es-query/src/filters/lib/index.ts
Normal file
|
@ -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;
|
100
packages/kbn-es-query/src/filters/lib/meta_filter.ts
Normal file
100
packages/kbn-es-query/src/filters/lib/meta_filter.ts
Normal file
|
@ -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);
|
||||
}
|
30
packages/kbn-es-query/src/filters/lib/phrase_filter.ts
Normal file
30
packages/kbn-es-query/src/filters/lib/phrase_filter.ts
Normal file
|
@ -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;
|
||||
};
|
28
packages/kbn-es-query/src/filters/lib/phrases_filter.ts
Normal file
28
packages/kbn-es-query/src/filters/lib/phrases_filter.ts
Normal file
|
@ -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;
|
||||
};
|
26
packages/kbn-es-query/src/filters/lib/query_string_filter.ts
Normal file
26
packages/kbn-es-query/src/filters/lib/query_string_filter.ts
Normal file
|
@ -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;
|
||||
};
|
35
packages/kbn-es-query/src/filters/lib/range_filter.ts
Normal file
35
packages/kbn-es-query/src/filters/lib/range_filter.ts
Normal file
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
1
packages/kbn-es-query/src/index.d.ts
vendored
1
packages/kbn-es-query/src/index.d.ts
vendored
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
export * from './kuery';
|
||||
export * from './filters';
|
||||
|
|
114
packages/kbn-es-query/tasks/build_cli.js
Normal file
114
packages/kbn-es-query/tasks/build_cli.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const { resolve } = require('path');
|
||||
|
||||
const getopts = require('getopts');
|
||||
const del = require('del');
|
||||
const supportsColor = require('supports-color');
|
||||
const { ToolingLog, withProcRunner, pickLevelFromFlags } = require('@kbn/dev-utils');
|
||||
|
||||
const ROOT_DIR = resolve(__dirname, '..');
|
||||
const BUILD_DIR = resolve(ROOT_DIR, 'target');
|
||||
|
||||
const padRight = (width, str) =>
|
||||
str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`;
|
||||
|
||||
const unknownFlags = [];
|
||||
const flags = getopts(process.argv, {
|
||||
boolean: ['watch', 'help', 'source-maps'],
|
||||
unknown(name) {
|
||||
unknownFlags.push(name);
|
||||
},
|
||||
});
|
||||
|
||||
const log = new ToolingLog({
|
||||
level: pickLevelFromFlags(flags),
|
||||
writeTo: process.stdout,
|
||||
});
|
||||
|
||||
if (unknownFlags.length) {
|
||||
log.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
|
||||
flags.help = true;
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (flags.help) {
|
||||
log.info(`
|
||||
Simple build tool for @kbn/es-query package
|
||||
|
||||
--watch Run in watch mode
|
||||
--source-maps Include sourcemaps
|
||||
--help Show this message
|
||||
`);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
withProcRunner(log, async proc => {
|
||||
log.info('Deleting old output');
|
||||
await del(BUILD_DIR);
|
||||
|
||||
const cwd = ROOT_DIR;
|
||||
const env = { ...process.env };
|
||||
if (supportsColor.stdout) {
|
||||
env.FORCE_COLOR = 'true';
|
||||
}
|
||||
|
||||
log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`);
|
||||
await Promise.all([
|
||||
...['public', 'server'].map(subTask =>
|
||||
proc.run(padRight(12, `babel:${subTask}`), {
|
||||
cmd: 'babel',
|
||||
args: [
|
||||
'src',
|
||||
'--out-dir',
|
||||
resolve(BUILD_DIR, subTask),
|
||||
'--extensions',
|
||||
'.js,.tsx',
|
||||
...(flags.watch ? ['--watch'] : ['--quiet']),
|
||||
...(flags['source-maps'] ? ['--source-map', 'inline'] : []),
|
||||
],
|
||||
wait: true,
|
||||
cwd,
|
||||
})
|
||||
),
|
||||
|
||||
...['public', 'server'].map(subTask =>
|
||||
proc.run(padRight(12, `tsc:${subTask}`), {
|
||||
cmd: 'tsc',
|
||||
args: [
|
||||
'--project',
|
||||
subTask === 'public'
|
||||
? 'tsconfig.browser.json'
|
||||
: 'tsconfig.json',
|
||||
...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []),
|
||||
...(flags['source-maps'] ? ['--inlineSourceMap', 'true'] : []),
|
||||
],
|
||||
wait: true,
|
||||
env,
|
||||
cwd,
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
log.success('Complete');
|
||||
}).catch(error => {
|
||||
log.error(error);
|
||||
process.exit(1);
|
||||
});
|
11
packages/kbn-es-query/tsconfig.browser.json
Normal file
11
packages/kbn-es-query/tsconfig.browser.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.browser.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./target/public"
|
||||
},
|
||||
"include": [
|
||||
"index.d.ts",
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./target/server"
|
||||
},
|
||||
"include": [
|
||||
"index.d.ts",
|
||||
"src/**/*.d.ts"
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
};
|
|
@ -19,7 +19,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<filter-bar index-patterns="[contextApp.indexPattern]"></filter-bar>
|
||||
<filter-bar
|
||||
class-name="'globalFilterGroup__filterBar'"
|
||||
filters="contextApp.state.queryParameters.filters"
|
||||
on-filters-updated="contextApp.actions.updateFilters"
|
||||
index-patterns="[contextApp.indexPattern]"
|
||||
></filter-bar>
|
||||
|
||||
<!-- Error feedback -->
|
||||
<div
|
||||
|
|
|
@ -22,6 +22,7 @@ import _ from 'lodash';
|
|||
import { callAfterBindingsWorkaround } from 'ui/compat';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import contextAppTemplate from './app.html';
|
||||
import 'ui/filter_bar';
|
||||
import './components/loading_button';
|
||||
import './components/size_picker/size_picker';
|
||||
import { getFirstSortableField } from './api/utils/sorting';
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import {
|
||||
MAX_CONTEXT_SIZE,
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
|
||||
|
||||
export function QueryParameterActionsProvider(indexPatterns, Private) {
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
|
||||
const setPredecessorCount = (state) => (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,
|
||||
|
|
|
@ -30,22 +30,25 @@
|
|||
|
||||
<!-- Search. -->
|
||||
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
|
||||
<query-bar
|
||||
<search-bar
|
||||
query="model.query"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
app-name="'dashboard'"
|
||||
on-submit="updateQueryAndFetch"
|
||||
index-patterns="indexPatterns"
|
||||
></query-bar>
|
||||
filters="model.filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
show-filter-bar="showFilterBar()"
|
||||
watch-depth="reference"
|
||||
></search-bar>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
<!-- Filters. -->
|
||||
<filter-bar
|
||||
ng-show="showFilterBar()"
|
||||
state="state"
|
||||
index-patterns="indexPatterns"
|
||||
></filter-bar>
|
||||
<apply-filters-popover
|
||||
filters="appState.$newFilters"
|
||||
on-cancel="onCancelApplyFilters"
|
||||
on-submit="onApplyFilters"
|
||||
></apply-filters-popover>
|
||||
|
||||
<div
|
||||
ng-show="getShouldShowEditHelp()"
|
||||
|
|
|
@ -24,7 +24,8 @@ import { uiModules } from 'ui/modules';
|
|||
import chrome from 'ui/chrome';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import 'ui/query_bar';
|
||||
import 'ui/search_bar';
|
||||
import 'ui/apply_filters';
|
||||
|
||||
import { panelActionsStore } from './store/panel_actions_store';
|
||||
|
||||
|
@ -75,6 +76,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
const confirmModal = $injector.get('confirmModal');
|
||||
const config = $injector.get('config');
|
||||
const Private = $injector.get('Private');
|
||||
const indexPatterns = $injector.get('indexPatterns');
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
|
@ -90,7 +92,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
i18n,
|
||||
) {
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const filterBar = Private(FilterBarQueryFilterProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
const embeddableFactories = Private(EmbeddableFactoriesRegistryProvider);
|
||||
const panelActionsRegistry = Private(ContextMenuActionsRegistryProvider);
|
||||
|
@ -130,12 +132,24 @@ app.directive('dashboardApp', function ($injector) {
|
|||
// https://github.com/angular/angular.js/wiki/Understanding-Scopes
|
||||
$scope.model = {
|
||||
query: dashboardStateManager.getQuery(),
|
||||
filters: queryFilter.getFilters(),
|
||||
timeRestore: dashboardStateManager.getTimeRestore(),
|
||||
title: dashboardStateManager.getTitle(),
|
||||
description: dashboardStateManager.getDescription(),
|
||||
};
|
||||
$scope.panels = dashboardStateManager.getPanels();
|
||||
$scope.indexPatterns = dashboardStateManager.getPanelIndexPatterns();
|
||||
|
||||
const panelIndexPatterns = dashboardStateManager.getPanelIndexPatterns();
|
||||
if (panelIndexPatterns && panelIndexPatterns.length > 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) => {
|
||||
|
@ -349,7 +383,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);
|
||||
|
@ -460,12 +494,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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -32,23 +32,20 @@
|
|||
|
||||
<!-- Search. -->
|
||||
<div data-transclude-slot="bottomRow" class="fullWidth">
|
||||
<query-bar
|
||||
<search-bar
|
||||
query="state.query"
|
||||
on-submit="updateQueryAndFetch"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
app-name="'discover'"
|
||||
index-patterns="[indexPattern]"
|
||||
></query-bar>
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
watch-depth="reference"
|
||||
></search-bar>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
<main class="container-fluid">
|
||||
<div class="row">
|
||||
<filter-bar
|
||||
state="state"
|
||||
index-patterns="[indexPattern]"
|
||||
></filter-bar>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 sidebar-container collapsible-sidebar" id="discover-sidebar">
|
||||
<disc-field-chooser
|
||||
|
@ -94,7 +91,7 @@
|
|||
ng-hide="fetchError"
|
||||
class="dscOverlay"
|
||||
>
|
||||
<div class="euiTitle" >
|
||||
<div class="euiTitle">
|
||||
<h2
|
||||
i18n-id="kbn.discover.searchingTitle"
|
||||
i18n-default-message="Searching"
|
||||
|
@ -142,7 +139,7 @@
|
|||
ng-options="interval.val as interval.display for interval in intervalOptions | filter: intervalEnabled"
|
||||
ng-blur="toggleInterval()"
|
||||
data-test-subj="discoverIntervalSelect"
|
||||
>
|
||||
>
|
||||
</select>
|
||||
<span ng-if="bucketInterval.scaled">
|
||||
<icon-tip
|
||||
|
@ -163,9 +160,9 @@
|
|||
</header>
|
||||
|
||||
<div id="discoverHistogram"
|
||||
ng-show="vis && rows.length !== 0"
|
||||
style="display: flex; height: 200px"
|
||||
>
|
||||
ng-show="vis && rows.length !== 0"
|
||||
style="display: flex; height: 200px"
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -37,26 +37,42 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="vis.type.requiresSearch && vis.type.options.showQueryBar" class="fullWidth kuiVerticalRhythmSmall">
|
||||
<query-bar
|
||||
<div class="fullWidth kuiVerticalRhythmSmall">
|
||||
<search-bar
|
||||
query="state.query"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
app-name="'visualize'"
|
||||
on-submit="updateQueryAndFetch"
|
||||
disable-auto-focus="true"
|
||||
index-patterns="[indexPattern]"
|
||||
></query-bar>
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
show-query-bar="vis.type.requiresSearch && vis.type.options.showQueryBar"
|
||||
show-filter-bar="vis.type.options.showFilterBar && chrome.getVisible()"
|
||||
watch-depth="reference"
|
||||
></search-bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
<!-- Filters. -->
|
||||
<!--
|
||||
The top nav is hidden in embed mode but the filter bar must still be present so
|
||||
we show the filter bar on its own here if the chrome is not visible.
|
||||
-->
|
||||
<filter-bar
|
||||
ng-if="vis.type.options.showFilterBar"
|
||||
state="state"
|
||||
ng-if="vis.type.options.showFilterBar && !chrome.getVisible()"
|
||||
class-name="'globalFilterGroup__filterBar'"
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
index-patterns="[indexPattern]"
|
||||
></filter-bar>
|
||||
|
||||
<apply-filters-popover
|
||||
key="applyFiltersKey"
|
||||
filters="state.$newFilters"
|
||||
on-cancel="onCancelApplyFilters"
|
||||
on-submit="onApplyFilters"
|
||||
></apply-filters-popover>
|
||||
|
||||
<div
|
||||
class="euiCallOut euiCallOut--primary euiCallOut--small hide-for-sharing"
|
||||
ng-if="vis.type.shouldMarkAsExperimentalInUI()"
|
||||
|
|
|
@ -23,7 +23,8 @@ import './visualization_editor';
|
|||
import 'ui/vis/editors/default/sidebar';
|
||||
import 'ui/visualize';
|
||||
import 'ui/collapsible_sidebar';
|
||||
import 'ui/query_bar';
|
||||
import 'ui/search_bar';
|
||||
import 'ui/apply_filters';
|
||||
import chrome from 'ui/chrome';
|
||||
import React from 'react';
|
||||
import angular from 'angular';
|
||||
|
@ -287,6 +288,28 @@ function VisEditor(
|
|||
return appState;
|
||||
}());
|
||||
|
||||
$scope.filters = queryFilter.getFilters();
|
||||
|
||||
$scope.onFiltersUpdated = filters => {
|
||||
// The filters will automatically be set when the queryFilter emits an update event (see below)
|
||||
queryFilter.setFilters(filters);
|
||||
};
|
||||
|
||||
$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;
|
||||
|
@ -355,6 +378,7 @@ function VisEditor(
|
|||
|
||||
// update the searchSource when filters update
|
||||
$scope.$listen(queryFilter, 'update', function () {
|
||||
$scope.filters = queryFilter.getFilters();
|
||||
$scope.fetch();
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
128
src/ui/public/apply_filters/apply_filters_popover.tsx
Normal file
128
src/ui/public/apply_filters/apply_filters_popover.tsx
Normal file
|
@ -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<Props, State> {
|
||||
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 = (
|
||||
<EuiForm>
|
||||
{this.props.filters.map((filter, i) => (
|
||||
<EuiFormRow key={i}>
|
||||
<EuiSwitch
|
||||
label={getFilterDisplayText(filter)}
|
||||
checked={this.isFilterSelected(i)}
|
||||
onChange={() => this.toggleFilterSelected(i)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
))}
|
||||
</EuiForm>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={this.props.onCancel}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage
|
||||
id="common.ui.applyFilters.popupHeader"
|
||||
defaultMessage="Select filters to apply"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>{form}</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={this.props.onCancel}>
|
||||
<FormattedMessage
|
||||
id="common.ui.applyFiltersPopup.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiButton onClick={this.onSubmit} fill>
|
||||
<FormattedMessage
|
||||
id="common.ui.applyFiltersPopup.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
7
src/ui/public/apply_filters/directive.html
Normal file
7
src/ui/public/apply_filters/directive.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<apply-filters-popover-component
|
||||
ng-if="state.key"
|
||||
key="state.key"
|
||||
filters="state.filters"
|
||||
on-cancel="onCancel"
|
||||
on-submit="onSubmit"
|
||||
></apply-filters-popover-component>
|
59
src/ui/public/apply_filters/directive.js
Normal file
59
src/ui/public/apply_filters/directive.js
Normal file
|
@ -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(),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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';
|
|
@ -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');
|
||||
|
|
20
src/ui/public/documentation_links/index.d.ts
vendored
Normal file
20
src/ui/public/documentation_links/index.d.ts
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export function getDocLink(id: string): string;
|
|
@ -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('<filter-bar></filter-bar>')($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 = `
|
||||
<div
|
||||
class="filter-bar filter-panel"
|
||||
ng-class="filterNavToggle.isOpen == true ? '' : 'filter-panel-close'">
|
||||
<div
|
||||
class="filter-link pull-right"
|
||||
ng-class="filterNavToggle.isOpen == true ? '' : 'action-show'"
|
||||
ng-show="filters.length">
|
||||
</div>
|
||||
<div
|
||||
class="filter-nav-link__icon"
|
||||
tooltip="{{ filterNavToggle.tooltipContent }}"
|
||||
tooltip-placement="left"
|
||||
tooltip-popup-delay="0"
|
||||
tooltip-append-to-body="1"
|
||||
ng-show="filters.length"
|
||||
ng-class="filterNavToggle.isOpen == true ? '' : 'filter-nav-link--close'"
|
||||
aria-hidden="!filters.length"
|
||||
>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
22
src/ui/public/filter_bar/_global_filter_group.scss
Normal file
22
src/ui/public/filter_bar/_global_filter_group.scss
Normal file
|
@ -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;
|
||||
}
|
37
src/ui/public/filter_bar/_global_filter_item.scss
Normal file
37
src/ui/public/filter_bar/_global_filter_item.scss
Normal file
|
@ -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;
|
||||
}
|
2
src/ui/public/filter_bar/_index.scss
Normal file
2
src/ui/public/filter_bar/_index.scss
Normal file
|
@ -0,0 +1,2 @@
|
|||
@import 'global_filter_group';
|
||||
@import 'global_filter_item';
|
|
@ -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));
|
||||
});
|
|
@ -1,229 +0,0 @@
|
|||
<section
|
||||
aria-label="{{ ::'common.ui.filterBar.filtersAriaLabel' | i18n: { defaultMessage: 'Filters' } }}"
|
||||
>
|
||||
<div class="filter-bar-confirm" ng-show="newFilters.length">
|
||||
<form ng-submit="applyFilters(newFilters)">
|
||||
<ul class="list-unstyled">
|
||||
<li
|
||||
i18n-id="common.ui.filterBar.applyTheseFiltersLabel"
|
||||
i18n-default-message="Apply these filters?"
|
||||
></li>
|
||||
<li ng-repeat="filter in newFilters track by $index" class="filter" ng-click="filter.meta.apply = !filter.meta.apply"><input type="checkbox" ng-checked="filter.meta.apply"/>
|
||||
<span
|
||||
ng-if="filter.meta.negate"
|
||||
i18n-id="common.ui.filterBar.notFilterLabel"
|
||||
i18n-default-message="NOT"
|
||||
></span>
|
||||
{{ filter.meta.key }}: {{ filter.meta.value }}
|
||||
</li>
|
||||
<li ng-if="changeTimeFilter" class="changeTimeFilter filter" ng-click="changeTimeFilter.meta.apply = !changeTimeFilter.meta.apply"><input type="checkbox" ng-checked="changeTimeFilter.meta.apply"/>
|
||||
<strong
|
||||
i18n-id="common.ui.filterBar.changeTimeToLabel"
|
||||
i18n-default-message="Change time to:"
|
||||
></strong>
|
||||
{{changeTimeFilter.meta.value}}
|
||||
</li>
|
||||
<li>
|
||||
<div class="kuiButtonGroup">
|
||||
<button
|
||||
class="kuiButton kuiButton--primary kuiButton--small"
|
||||
data-test-subj="filterBarApplyFilters"
|
||||
i18n-id="common.ui.filterBar.applyNowButtonLabel"
|
||||
i18n-default-message="Apply Now"
|
||||
></button>
|
||||
|
||||
<button
|
||||
class="kuiButton kuiButton--hollow"
|
||||
ng-click="clearFilterBar();"
|
||||
i18n-id="common.ui.filterBar.cancelButtonLabel"
|
||||
i18n-default-message="Cancel"
|
||||
></button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="filter-bar filter-panel"
|
||||
ng-class="filterNavToggle.isOpen == true || !showCollapseLink() ? '' : 'filter-panel--close'"
|
||||
>
|
||||
<div class="filter-panel__pill">
|
||||
<filter-pill
|
||||
ng-repeat="filter in filters track by $index"
|
||||
filter="filter"
|
||||
on-toggle-filter="toggleFilter"
|
||||
on-pin-filter="pinFilter"
|
||||
on-invert-filter="invertFilter"
|
||||
on-delete-filter="deleteFilter"
|
||||
on-edit-filter="editFilter"
|
||||
></filter-pill>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="filter-link"
|
||||
>
|
||||
<div class="filter-description small">
|
||||
<button
|
||||
ng-click="addFilter()"
|
||||
class="euiLink euiLink--primary"
|
||||
data-test-subj="addFilter"
|
||||
>
|
||||
<span
|
||||
i18n-id="common.ui.filterBar.addFilterButtonLabel"
|
||||
i18n-default-message="Add a filter"
|
||||
></span>
|
||||
<span class="fa fa-plus"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="filter-link pull-right"
|
||||
ng-class="filterNavToggle.isOpen == true || !showCollapseLink() ? '' : 'action-show'"
|
||||
ng-show="filters.length"
|
||||
>
|
||||
<div class="filter-description small">
|
||||
<a
|
||||
ng-click="showFilterActions = !showFilterActions"
|
||||
kbn-accessible-click
|
||||
aria-expanded="{{!!showFilterActions}}"
|
||||
aria-controls="filterActionsAllContainer"
|
||||
>
|
||||
<span
|
||||
i18n-id="common.ui.filterBar.actionsButtonLabel"
|
||||
i18n-default-message="Actions"
|
||||
></span>
|
||||
<span
|
||||
class="fa"
|
||||
ng-class="{
|
||||
'fa-caret-down': showFilterActions,
|
||||
'fa-caret-right': !showFilterActions
|
||||
}"
|
||||
data-test-subj="showFilterActions"
|
||||
></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="filter-nav-link__icon"
|
||||
tooltip="{{ filterNavToggle.tooltipContent }}"
|
||||
tooltip-placement="left"
|
||||
tooltip-popup-delay="0"
|
||||
tooltip-append-to-body="1"
|
||||
ng-show="filters.length && showCollapseLink()"
|
||||
aria-hidden="!filters.length || !showCollapseLink()"
|
||||
>
|
||||
<button
|
||||
class="filter-nav-link__collapser"
|
||||
ng-click="toggleFilterShown($event)"
|
||||
aria-expanded="true"
|
||||
aria-label="{{ ::'common.ui.filterBar.toggleFilterbarAriaLabel' | i18n: { defaultMessage: 'Toggle filterbar' } }}"
|
||||
>
|
||||
<span class="kuiIcon" ng-class="filterNavToggle.isOpen == true ? 'fa-chevron-circle-up' : 'fa-chevron-circle-down'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="filter-edit-container"
|
||||
ng-if="editingFilter"
|
||||
>
|
||||
<filter-editor
|
||||
filter="editingFilter"
|
||||
index-patterns="indexPatterns"
|
||||
on-delete="deleteFilter(editingFilter)"
|
||||
on-save="saveEdit(filter, newFilter, isPinned)"
|
||||
on-cancel="cancelEdit()"
|
||||
></filter-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="filter-bar filter-bar-condensed"
|
||||
ng-show="filters.length && showFilterActions"
|
||||
id="filterActionsAllContainer"
|
||||
>
|
||||
<div class="filter-actions-all">
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<strong
|
||||
i18n-id="common.ui.filterBar.allFiltersLabel"
|
||||
i18n-default-message="All filters:"
|
||||
></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<a
|
||||
ng-click="toggleAll(false)"
|
||||
kbn-accessible-click
|
||||
i18n-id="common.ui.filterBar.allFilters.enableLabel"
|
||||
i18n-default-message="Enable"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<a
|
||||
ng-click="toggleAll(true)"
|
||||
kbn-accessible-click
|
||||
i18n-id="common.ui.filterBar.allFilters.disableLabel"
|
||||
i18n-default-message="Disable"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<a
|
||||
ng-click="pinAll(true)"
|
||||
kbn-accessible-click
|
||||
i18n-id="common.ui.filterBar.allFilters.pinLabel"
|
||||
i18n-default-message="Pin"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<a
|
||||
ng-click="pinAll(false)"
|
||||
kbn-accessible-click
|
||||
i18n-id="common.ui.filterBar.allFilters.unpinLabel"
|
||||
i18n-default-message="Unpin"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<a
|
||||
ng-click="invertAll()"
|
||||
kbn-accessible-click
|
||||
i18n-id="common.ui.filterBar.allFilters.invertLabel"
|
||||
i18n-default-message="Invert"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<a
|
||||
ng-click="toggleAll()"
|
||||
kbn-accessible-click
|
||||
i18n-id="common.ui.filterBar.allFilters.toggleLabel"
|
||||
i18n-default-message="Toggle"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-link">
|
||||
<div class="filter-description">
|
||||
<a
|
||||
ng-click="removeAll()"
|
||||
data-test-subj="removeAllFilters"
|
||||
kbn-accessible-click
|
||||
i18n-id="common.ui.filterBar.allFilters.removeLabel"
|
||||
i18n-default-message="Remove"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -1,218 +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, i18n) {
|
||||
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;
|
||||
};
|
||||
|
||||
const collapseFilterTooltip = i18n('common.ui.filterBar.collapseFilterTooltip', {
|
||||
defaultMessage: 'Collapse filter bar \n to show less'
|
||||
});
|
||||
const expandFilterTooltip = i18n('common.ui.filterBar.expandFilterTooltip', { defaultMessage: 'Expand filter bar \n to show more' });
|
||||
|
||||
$scope.filterNavToggle = {
|
||||
isOpen: true,
|
||||
tooltipContent: collapseFilterTooltip
|
||||
};
|
||||
|
||||
$scope.toggleFilterShown = () => {
|
||||
const collapser = $elem.find('.filter-nav-link__collapser');
|
||||
const filterPanelPill = $elem.find('.filter-panel__pill');
|
||||
if ($scope.filterNavToggle.isOpen) {
|
||||
$scope.filterNavToggle.tooltipContent = expandFilterTooltip;
|
||||
collapser.attr('aria-expanded', 'false');
|
||||
filterPanelPill.attr('style', 'width: calc(100% - 80px)');
|
||||
} else {
|
||||
$scope.filterNavToggle.tooltipContent = collapseFilterTooltip;
|
||||
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();
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
217
src/ui/public/filter_bar/filter_bar.tsx
Normal file
217
src/ui/public/filter_bar/filter_bar.tsx
Normal file
|
@ -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<Props, State> {
|
||||
public state = {
|
||||
isAddFilterPopoverOpen: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
const classes = classNames('globalFilterBar', this.props.className);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className="globalFilterGroup"
|
||||
gutterSize="none"
|
||||
alignItems="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem className="globalFilterGroup__branch" grow={false}>
|
||||
<FilterOptions
|
||||
onEnableAll={this.onEnableAll}
|
||||
onDisableAll={this.onDisableAll}
|
||||
onPinAll={this.onPinAll}
|
||||
onUnpinAll={this.onUnpinAll}
|
||||
onToggleAllNegated={this.onToggleAllNegated}
|
||||
onToggleAllDisabled={this.onToggleAllDisabled}
|
||||
onRemoveAll={this.onRemoveAll}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
className={classes}
|
||||
wrap={true}
|
||||
responsive={false}
|
||||
gutterSize="xs"
|
||||
alignItems="center"
|
||||
>
|
||||
{this.renderItems()}
|
||||
{this.renderAddFilter()}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
private renderItems() {
|
||||
return this.props.filters.map((filter, i) => (
|
||||
<EuiFlexItem key={i} grow={false}>
|
||||
<FilterItem
|
||||
id={`${i}`}
|
||||
filter={filter}
|
||||
onUpdate={newFilter => this.onUpdate(i, newFilter)}
|
||||
onRemove={() => this.onRemove(i)}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
));
|
||||
}
|
||||
|
||||
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 = (
|
||||
<EuiButtonEmpty size="s" onClick={this.onOpenAddFilterPopover} data-test-subj="addFilter">
|
||||
+{' '}
|
||||
<FormattedMessage
|
||||
id="common.ui.filterBar.addFilterButtonLabel"
|
||||
defaultMessage="Add filter"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="addFilterPopover"
|
||||
button={button}
|
||||
isOpen={this.state.isAddFilterPopoverOpen}
|
||||
closePopover={this.onCloseAddFilterPopover}
|
||||
anchorPosition="downLeft"
|
||||
withTitle
|
||||
panelPaddingSize="none"
|
||||
ownFocus={true}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div style={{ width: 400 }}>
|
||||
<FilterEditor
|
||||
filter={newFilter}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
onSubmit={this.onAdd}
|
||||
onCancel={this.onCloseAddFilterPopover}
|
||||
/>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
61
src/ui/public/filter_bar/filter_editor/generic_combo_box.tsx
Normal file
61
src/ui/public/filter_bar/filter_editor/generic_combo_box.tsx
Normal file
|
@ -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<T> {
|
||||
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<T>(props: GenericComboBoxProps<T>) {
|
||||
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 (
|
||||
<EuiComboBox
|
||||
options={euiOptions}
|
||||
selectedOptions={selectedEuiOptions}
|
||||
onChange={onComboBoxChange}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
471
src/ui/public/filter_bar/filter_editor/index.tsx
Normal file
471
src/ui/public/filter_bar/filter_editor/index.tsx
Normal file
|
@ -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<Props, State> {
|
||||
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 (
|
||||
<div>
|
||||
<EuiPopoverTitle>
|
||||
<EuiFlexGroup alignItems="baseline">
|
||||
<EuiFlexItem>
|
||||
<FormattedMessage
|
||||
id="common.ui.filterEditor.editFilterPopupTitle"
|
||||
defaultMessage="Edit filter"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty size="xs" onClick={this.toggleCustomEditor}>
|
||||
{this.state.isCustomEditorOpen ? (
|
||||
<FormattedMessage
|
||||
id="common.ui.filterEditor.editFilterValuesButtonLabel"
|
||||
defaultMessage="Edit filter values"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="common.ui.filterEditor.editQueryDslButtonLabel"
|
||||
defaultMessage="Edit as Query DSL"
|
||||
/>
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
|
||||
<div className="globalFilterItem__editorForm">
|
||||
<EuiForm>
|
||||
{this.renderIndexPatternInput()}
|
||||
|
||||
{this.state.isCustomEditorOpen ? this.renderCustomEditor() : this.renderRegularEditor()}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiSwitch
|
||||
id="filterEditorCustomLabelSwitch"
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.createCustomLabelSwitchLabel',
|
||||
defaultMessage: 'Create custom label?',
|
||||
})}
|
||||
checked={this.state.useCustomLabel}
|
||||
onChange={this.onCustomLabelSwitchChange}
|
||||
/>
|
||||
|
||||
{this.state.useCustomLabel && (
|
||||
<div>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.createCustomLabelInputLabel',
|
||||
defaultMessage: 'Custom label',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={`${this.state.customLabel}`}
|
||||
onChange={this.onCustomLabelChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<EuiFlexGroup direction="rowReverse" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={this.onSubmit}
|
||||
isDisabled={!this.isFilterValid()}
|
||||
data-test-subj="saveFilter"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="common.ui.filterEditor.saveButtonLabel"
|
||||
defaultMessage="Save"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
flush="right"
|
||||
onClick={this.props.onCancel}
|
||||
data-test-subj="cancelSaveFilter"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="common.ui.filterEditor.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem />
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderIndexPatternInput() {
|
||||
if (this.props.indexPatterns.length <= 1) {
|
||||
return '';
|
||||
}
|
||||
const { selectedIndexPattern } = this.state;
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.indexPatternSelectLabel',
|
||||
defaultMessage: 'Index Pattern',
|
||||
})}
|
||||
>
|
||||
<IndexPatternComboBox
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterBar.indexPatternSelectPlaceholder',
|
||||
defaultMessage: 'Select an index pattern',
|
||||
})}
|
||||
options={this.props.indexPatterns}
|
||||
selectedOptions={selectedIndexPattern ? [selectedIndexPattern] : []}
|
||||
getLabel={indexPattern => indexPattern.title}
|
||||
onChange={this.onIndexPatternChange}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
isClearable={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
private renderRegularEditor() {
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem style={{ maxWidth: '188px' }}>{this.renderFieldInput()}</EuiFlexItem>
|
||||
<EuiFlexItem style={{ maxWidth: '188px' }}>{this.renderOperatorInput()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<div data-test-subj="filterParams">{this.renderParamsEditor()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderFieldInput() {
|
||||
const { selectedIndexPattern, selectedField } = this.state;
|
||||
const fields = selectedIndexPattern ? getFilterableFields(selectedIndexPattern) : [];
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.fieldSelectLabel',
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
>
|
||||
<FieldComboBox
|
||||
id="fieldInput"
|
||||
isDisabled={!selectedIndexPattern}
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.fieldSelectPlaceholder',
|
||||
defaultMessage: 'Select a field',
|
||||
})}
|
||||
options={fields}
|
||||
selectedOptions={selectedField ? [selectedField] : []}
|
||||
getLabel={field => field.name}
|
||||
onChange={this.onFieldChange}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
isClearable={false}
|
||||
data-test-subj="filterFieldSuggestionList"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
private renderOperatorInput() {
|
||||
const { selectedField, selectedOperator } = this.state;
|
||||
const operators = selectedField ? getOperatorOptions(selectedField) : [];
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.operatorSelectLabel',
|
||||
defaultMessage: 'Operator',
|
||||
})}
|
||||
>
|
||||
<OperatorComboBox
|
||||
isDisabled={!selectedField}
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.operatorSelectPlaceholder',
|
||||
defaultMessage: 'Select an operator',
|
||||
})}
|
||||
options={operators}
|
||||
selectedOptions={selectedOperator ? [selectedOperator] : []}
|
||||
getLabel={({ message }) => message}
|
||||
onChange={this.onOperatorChange}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
isClearable={false}
|
||||
data-test-subj="filterOperatorList"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCustomEditor() {
|
||||
return (
|
||||
<EuiFormRow label="Value">
|
||||
<EuiCodeEditor
|
||||
value={this.state.queryDsl}
|
||||
onChange={this.onQueryDslChange}
|
||||
mode="json"
|
||||
width="100%"
|
||||
height="250px"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
private renderParamsEditor() {
|
||||
const indexPattern = this.state.selectedIndexPattern;
|
||||
if (!indexPattern || !this.state.selectedOperator) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (this.state.selectedOperator.type) {
|
||||
case 'exists':
|
||||
return '';
|
||||
case 'phrase':
|
||||
return (
|
||||
<PhraseValueInput
|
||||
indexPattern={indexPattern}
|
||||
field={this.state.selectedField}
|
||||
value={this.state.params}
|
||||
onChange={this.onParamsChange}
|
||||
data-test-subj="phraseValueInput"
|
||||
/>
|
||||
);
|
||||
case 'phrases':
|
||||
return (
|
||||
<PhrasesValuesInput
|
||||
indexPattern={indexPattern}
|
||||
field={this.state.selectedField}
|
||||
values={this.state.params}
|
||||
onChange={this.onParamsChange}
|
||||
/>
|
||||
);
|
||||
case 'range':
|
||||
return (
|
||||
<RangeValueInput
|
||||
field={this.state.selectedField}
|
||||
value={this.state.params}
|
||||
onChange={this.onParamsChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
const useCustomLabel = event.target.checked;
|
||||
const customLabel = event.target.checked ? '' : null;
|
||||
this.setState({ useCustomLabel, customLabel });
|
||||
};
|
||||
|
||||
private onCustomLabelChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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<IndexPattern>) {
|
||||
return GenericComboBox(props);
|
||||
}
|
||||
|
||||
function FieldComboBox(props: GenericComboBoxProps<Field>) {
|
||||
return GenericComboBox(props);
|
||||
}
|
||||
|
||||
function OperatorComboBox(props: GenericComboBoxProps<Operator>) {
|
||||
return GenericComboBox(props);
|
||||
}
|
||||
|
||||
export const FilterEditor = injectI18n(FilterEditorUI);
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
106
src/ui/public/filter_bar/filter_editor/lib/filter_operators.ts
Normal file
106
src/ui/public/filter_bar/filter_editor/lib/filter_operators.ts
Normal file
|
@ -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,
|
||||
];
|
|
@ -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'
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
73
src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx
Normal file
73
src/ui/public/filter_bar/filter_editor/phrase_suggestor.tsx
Normal file
|
@ -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<T extends PhraseSuggestorProps> 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 });
|
||||
}
|
||||
}
|
|
@ -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<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.valueInputLabel',
|
||||
defaultMessage: 'Value',
|
||||
})}
|
||||
>
|
||||
{this.isSuggestingValues() ? (
|
||||
this.renderWithSuggestions()
|
||||
) : (
|
||||
<ValueInputType
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.valueInputPlaceholder',
|
||||
defaultMessage: 'Enter a value',
|
||||
})}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
type={this.props.field ? this.props.field.type : 'string'}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
private renderWithSuggestions() {
|
||||
const { suggestions } = this.state;
|
||||
const { value, intl, onChange } = this.props;
|
||||
const options = value ? uniq([value, ...suggestions]) : suggestions;
|
||||
return (
|
||||
<StringComboBox
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.valueSelectPlaceholder',
|
||||
defaultMessage: 'Select a value',
|
||||
})}
|
||||
options={options}
|
||||
getLabel={option => 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<string>) {
|
||||
return GenericComboBox(props);
|
||||
}
|
||||
|
||||
export const PhraseValueInput = injectI18n(PhraseValueInputUI);
|
|
@ -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<Props> {
|
||||
public render() {
|
||||
const { suggestions } = this.state;
|
||||
const { values, intl, onChange } = this.props;
|
||||
const options = values ? uniq([...values, ...suggestions]) : suggestions;
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.valuesSelectLabel',
|
||||
defaultMessage: 'Values',
|
||||
})}
|
||||
>
|
||||
<StringComboBox
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.valuesSelectPlaceholder',
|
||||
defaultMessage: 'Select values',
|
||||
})}
|
||||
options={options}
|
||||
getLabel={option => option}
|
||||
selectedOptions={values || []}
|
||||
onCreateOption={(option: string) => onChange([...(values || []), option])}
|
||||
onChange={onChange}
|
||||
isClearable={false}
|
||||
data-test-subj="filterParamsComboBox phrasesParamsComboxBox"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function StringComboBox(props: GenericComboBoxProps<string>) {
|
||||
return GenericComboBox(props);
|
||||
}
|
||||
|
||||
export const PhrasesValuesInput = injectI18n(PhrasesValuesInputUI);
|
121
src/ui/public/filter_bar/filter_editor/range_value_input.tsx
Normal file
121
src/ui/public/filter_bar/filter_editor/range_value_input.tsx
Normal file
|
@ -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<RangeParams>;
|
||||
|
||||
interface Props {
|
||||
field?: Field;
|
||||
value?: RangeParams;
|
||||
onChange: (params: RangeParamsPartial) => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
class RangeValueInputUI extends Component<Props> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const type = this.props.field ? this.props.field.type : 'string';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup style={{ maxWidth: 600 }}>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.rangeStartInputLabel',
|
||||
defaultMessage: 'From',
|
||||
})}
|
||||
>
|
||||
<ValueInputType
|
||||
type={type}
|
||||
value={this.props.value ? this.props.value.from : undefined}
|
||||
onChange={this.onFromChange}
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.rangeStartInputPlaceholder',
|
||||
defaultMessage: 'Start of the range',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.rangeEndInputLabel',
|
||||
defaultMessage: 'To',
|
||||
})}
|
||||
>
|
||||
<ValueInputType
|
||||
type={type}
|
||||
value={this.props.value ? this.props.value.to : undefined}
|
||||
onChange={this.onToChange}
|
||||
placeholder={this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.rangeEndInputPlaceholder',
|
||||
defaultMessage: 'End of the range',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{type === 'date' ? (
|
||||
<EuiLink target="_window" href={getDocLink('date.dateMath')}>
|
||||
<FormattedMessage
|
||||
id="common.ui.filterEditor.dateFormatHelpLinkLabel"
|
||||
defaultMessage="Accepted date formats"
|
||||
/>{' '}
|
||||
<EuiIcon type="link" />
|
||||
</EuiLink>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
120
src/ui/public/filter_bar/filter_editor/value_input_type.tsx
Normal file
120
src/ui/public/filter_bar/filter_editor/value_input_type.tsx
Normal file
|
@ -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<Props> {
|
||||
public render() {
|
||||
const value = this.props.value;
|
||||
let inputElement: React.ReactNode;
|
||||
switch (this.props.type) {
|
||||
case 'string':
|
||||
inputElement = (
|
||||
<EuiFieldText
|
||||
placeholder={this.props.placeholder}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'number':
|
||||
inputElement = (
|
||||
<EuiFieldNumber
|
||||
placeholder={this.props.placeholder}
|
||||
value={typeof value === 'string' ? parseFloat(value) : value}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'date':
|
||||
inputElement = (
|
||||
<EuiFieldText
|
||||
placeholder={this.props.placeholder}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
isInvalid={!isEmpty(value) && !validateParams(value, this.props.type)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'ip':
|
||||
inputElement = (
|
||||
<EuiFieldText
|
||||
placeholder={this.props.placeholder}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
isInvalid={!isEmpty(value) && !validateParams(value, this.props.type)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'boolean':
|
||||
inputElement = (
|
||||
<EuiSelect
|
||||
options={[
|
||||
{ value: undefined, text: this.props.placeholder },
|
||||
{
|
||||
value: 'true',
|
||||
text: this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.trueOptionLabel',
|
||||
defaultMessage: 'true',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'false',
|
||||
text: this.props.intl.formatMessage({
|
||||
id: 'common.ui.filterEditor.falseOptionLabel',
|
||||
defaultMessage: 'false',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
value={value}
|
||||
onChange={this.onBoolChange}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return inputElement;
|
||||
}
|
||||
|
||||
private onBoolChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const boolValue = event.target.value === 'true';
|
||||
this.props.onChange(boolValue);
|
||||
};
|
||||
|
||||
private onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const params = event.target.value;
|
||||
this.props.onChange(params);
|
||||
};
|
||||
}
|
||||
|
||||
export const ValueInputType = injectI18n(ValueInputTypeUI);
|
226
src/ui/public/filter_bar/filter_item.tsx
Normal file
226
src/ui/public/filter_bar/filter_item.tsx
Normal file
|
@ -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<Props, State> {
|
||||
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 = (
|
||||
<FilterView
|
||||
filter={filter}
|
||||
className={classes}
|
||||
iconOnClick={() => 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: (
|
||||
<div>
|
||||
<FilterEditor
|
||||
filter={filter}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
onSubmit={this.onSubmit}
|
||||
onCancel={this.closePopover}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id={`popoverFor_filter${id}`}
|
||||
isOpen={this.state.isPopoverOpen}
|
||||
closePopover={this.closePopover}
|
||||
button={badge}
|
||||
anchorPosition="downCenter"
|
||||
withTitle={true}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenu initialPanelId={0} panels={panelTree} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
|
@ -1,101 +0,0 @@
|
|||
<div
|
||||
class="filter"
|
||||
ng-class="{ negate: pill.filter.meta.negate, disabled: pill.filter.meta.disabled }"
|
||||
data-test-subj="filter filter-{{ pill.filter.meta.disabled ? 'disabled' : 'enabled' }} {{ pill.filter.meta.key ? 'filter-key-' + pill.filter.meta.key : '' }} {{ pill.filter.meta.value ? 'filter-value-' + pill.filter.meta.value : '' }}"
|
||||
ng-mouseover="pill.activateActions()"
|
||||
ng-mouseleave="pill.deactivateActions()"
|
||||
>
|
||||
|
||||
<div
|
||||
class="filter-description"
|
||||
ng-class="{'filter-description-deactivated': pill.areActionsActivated}"
|
||||
tabindex="0"
|
||||
aria-disabled="{{pill.filter.meta.disabled}}"
|
||||
>
|
||||
<span ng-if="pill.filter.$state.store == 'globalState'"><span class="fa fa-fw fa-thumb-tack pinned"></span></span>
|
||||
<span
|
||||
ng-if="pill.filter.meta.negate"
|
||||
i18n-id="common.ui.filterBar.filterPill.negateLabel"
|
||||
i18n-default-message="NOT"
|
||||
></span>
|
||||
<span ng-if="pill.filter.meta.alias">{{ pill.filter.meta.alias }}</span>
|
||||
<span ng-if="!pill.filter.meta.alias">{{ pill.filter.meta.key }}:</span>
|
||||
<span ng-if="!pill.filter.meta.alias">"{{ pill.filter.meta.value }}"</span>
|
||||
</div>
|
||||
|
||||
<div class="filter-actions" ng-class="{'filter-actions-activated': pill.areActionsActivated}">
|
||||
<button
|
||||
class="action filter-toggle"
|
||||
ng-click="pill.onToggleFilter(pill.filter)"
|
||||
data-test-subj="disableFilter-{{ pill.filter.meta.key }}"
|
||||
ng-focus="pill.activateActions()"
|
||||
ng-blur="pill.deactivateActions()"
|
||||
aria-label="{{pill.filter.meta.disabled ? pill.i18n.enableFilterAriaLabel : pill.i18n.disableFilterAriaLabel}}"
|
||||
tooltip="{{pill.filter.meta.disabled ? pill.i18n.enableFilterTooltip : pill.i18n.disableFilterTooltip}}"
|
||||
tooltip-append-to-body="true"
|
||||
>
|
||||
<span ng-show="pill.filter.meta.disabled" class="fa fa-fw fa-square-o disabled"></span>
|
||||
<span ng-hide="pill.filter.meta.disabled" class="fa fa-fw fa-check-square-o enabled"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action filter-pin"
|
||||
ng-click="pill.onPinFilter(pill.filter)"
|
||||
data-test-subj="pinFilter-{{ pill.filter.meta.key }}"
|
||||
ng-focus="pill.activateActions()"
|
||||
ng-blur="pill.deactivateActions()"
|
||||
aria-label="{{pill.filter.$state.store == 'globalState' ? pill.i18n.unpinFilterAriaLabel : pill.i18n.pinFilterAriaLabel}}"
|
||||
tooltip="{{pill.filter.$state.store == 'globalState' ? pill.i18n.unpinFilterTooltip : pill.i18n.pinFilterTooltip}}"
|
||||
tooltip-append-to-body="true"
|
||||
>
|
||||
<span ng-show="pill.filter.$state.store == 'globalState'" class="fa fa-fw fa-thumb-tack pinned"></span>
|
||||
<span ng-hide="pill.filter.$state.store == 'globalState'" class="fa fa-fw fa-thumb-tack fa-rotate-270 unpinned"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action filter-invert"
|
||||
ng-click="pill.onInvertFilter(pill.filter)"
|
||||
data-test-subj="invertFilter-{{ pill.filter.meta.key }}"
|
||||
ng-focus="pill.activateActions()"
|
||||
ng-blur="pill.deactivateActions()"
|
||||
aria-label="{{pill.filter.meta.negate ? pill.i18n.includeMatchesAriaLabel : pill.i18n.excludeMatchesAriaLabel}}"
|
||||
tooltip="{{pill.filter.meta.negate ? pill.i18n.includeMatchesTooltip : pill.i18n.excludeMatchesTooltip}}"
|
||||
tooltip-append-to-body="true"
|
||||
>
|
||||
<span ng-show="pill.filter.meta.negate" class="fa fa-fw fa-search-plus negative"></span>
|
||||
<span ng-hide="pill.filter.meta.negate" class="fa fa-fw fa-search-minus positive"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action filter-remove"
|
||||
ng-click="pill.onDeleteFilter(pill.filter)"
|
||||
ng-focus="pill.activateActions()"
|
||||
ng-blur="pill.deactivateActions()"
|
||||
aria-label="{{ ::'common.ui.filterBar.filterPill.removeFilterAriaLabel' | i18n: { defaultMessage: 'Remove filter' } }}"
|
||||
tooltip="{{ ::'common.ui.filterBar.filterPill.removeFilterTooltip' | i18n: { defaultMessage: 'Remove filter' } }}"
|
||||
tooltip-append-to-body="true"
|
||||
>
|
||||
<span class="fa fa-fw fa-trash" data-test-subj="removeFilter-{{ pill.filter.meta.key }}"></span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="action filter-edit"
|
||||
ng-click="pill.onEditFilter(pill.filter)"
|
||||
ng-disabled="pill.isControlledByPanel()"
|
||||
ng-focus="pill.activateActions()"
|
||||
ng-blur="pill.deactivateActions()"
|
||||
aria-label="{{ ::'common.ui.filterBar.filterPill.editFilterAriaLabel' | i18n: { defaultMessage: 'Edit filter' } }}"
|
||||
tooltip="{{ ::'common.ui.filterBar.filterPill.editFilterTooltip' | i18n: { defaultMessage: 'Edit filter' } }}"
|
||||
tooltip-append-to-body="true"
|
||||
data-test-subj="editFilter"
|
||||
>
|
||||
<span
|
||||
ng-show="pill.isControlledByPanel()"
|
||||
tooltip="{{ ::'common.ui.filterBar.filterPill.editDisabledTooltip' | i18n: { defaultMessage: 'Edit disabled because filter is controlled by Kibana' } }}"
|
||||
class="fa fa-fw fa-edit fa-disabled"
|
||||
></span>
|
||||
<span ng-hide="pill.isControlledByPanel()" class="fa fa-fw fa-edit"></span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,71 +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 (i18n) {
|
||||
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');
|
||||
};
|
||||
|
||||
this.i18n = {
|
||||
enableFilterAriaLabel: i18n('common.ui.filterBar.filterPill.enableFilterAriaLabel', { defaultMessage: 'Enable filter' }),
|
||||
enableFilterTooltip: i18n('common.ui.filterBar.filterPill.enableFilterTooltip', { defaultMessage: 'Enable filter' }),
|
||||
disableFilterAriaLabel: i18n('common.ui.filterBar.filterPill.disableFilterAriaLabel', { defaultMessage: 'Disable filter' }),
|
||||
disableFilterTooltip: i18n('common.ui.filterBar.filterPill.disableFilterTooltip', { defaultMessage: 'Disable filter' }),
|
||||
pinFilterAriaLabel: i18n('common.ui.filterBar.filterPill.pinFilterAriaLabel', { defaultMessage: 'Pin filter' }),
|
||||
pinFilterTooltip: i18n('common.ui.filterBar.filterPill.pinFilterTooltip', { defaultMessage: 'Pin filter' }),
|
||||
unpinFilterAriaLabel: i18n('common.ui.filterBar.filterPill.unpinFilterAriaLabel', { defaultMessage: 'Unpin filter' }),
|
||||
unpinFilterTooltip: i18n('common.ui.filterBar.filterPill.unpinFilterTooltip', { defaultMessage: 'Unpin filter' }),
|
||||
includeMatchesAriaLabel: i18n('common.ui.filterBar.filterPill.includeMatchesAriaLabel', { defaultMessage: 'Include matches' }),
|
||||
includeMatchesTooltip: i18n('common.ui.filterBar.filterPill.includeMatchesTooltip', { defaultMessage: 'Include matches' }),
|
||||
excludeMatchesAriaLabel: i18n('common.ui.filterBar.filterPill.excludeMatchesAriaLabel', { defaultMessage: 'Exclude matches' }),
|
||||
excludeMatchesTooltip: i18n('common.ui.filterBar.filterPill.excludeMatchesTooltip', { defaultMessage: 'Exclude matches' }),
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
103
src/ui/public/filter_bar/filter_view/index.tsx
Normal file
103
src/ui/public/filter_bar/filter_view/index.tsx
Normal file
|
@ -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<Props> = ({ 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 (
|
||||
<EuiBadge
|
||||
title={title}
|
||||
iconType="cross"
|
||||
// @ts-ignore
|
||||
iconSide="right"
|
||||
closeButtonProps={{
|
||||
// Removing tab focus on close button because the same option can be optained through the context menu
|
||||
// Also, we may want to add a `DEL` keyboard press functionality
|
||||
tabIndex: '-1',
|
||||
}}
|
||||
iconOnClickAriaLabel={i18n.translate('common.ui.filterBar.filterItemBadgeIconAriaLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
onClickAriaLabel={i18n.translate('common.ui.filterBar.filterItemBadgeAriaLabel', {
|
||||
defaultMessage: 'Filter actions',
|
||||
})}
|
||||
{...rest}
|
||||
>
|
||||
<span>{getFilterDisplayText(filter)}</span>
|
||||
</EuiBadge>
|
||||
);
|
||||
};
|
||||
|
||||
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)}`;
|
||||
}
|
||||
}
|
22
src/ui/public/filter_bar/index.ts
Normal file
22
src/ui/public/filter_bar/index.ts
Normal file
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,173 +0,0 @@
|
|||
<div class="filterEditor kuiModal">
|
||||
<div class="kuiModalHeader">
|
||||
<div class="kuiModalHeader__title">
|
||||
<span
|
||||
ng-show="filterEditor.filter.meta.isNew"
|
||||
i18n-id="common.ui.filterEditor.addFilterLabel"
|
||||
i18n-default-message="Add filter"
|
||||
></span>
|
||||
<span ng-hide="filterEditor.filter.meta.isNew"
|
||||
i18n-id="common.ui.filterEditor.editFilterLabel"
|
||||
i18n-default-message="Edit filter"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
data-test-subj="filterEditorModalCloseButton"
|
||||
class="kuiModalHeaderCloseButton kuiIcon fa-times"
|
||||
ng-click="filterEditor.onCancel()"
|
||||
aria-label="{{ ::'common.ui.filterEditor.closeFilterPopoverAriaLabel' | i18n: { defaultMessage: 'Close filter popover' } }}"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="kuiModalBody">
|
||||
<!-- Filter definition -->
|
||||
<div class="kuiVerticalRhythm">
|
||||
<div class="kuiVerticalRhythmSmall filterEditor__labelBar">
|
||||
<label
|
||||
class="kuiLabel"
|
||||
i18n-id="common.ui.filterEditor.filterLabel"
|
||||
i18n-default-message="Filter"
|
||||
></label>
|
||||
|
||||
<div>
|
||||
<a
|
||||
class="kuiLink"
|
||||
ng-click="filterEditor.toggleEditingQueryDsl()"
|
||||
kbn-accessible-click
|
||||
>
|
||||
<span
|
||||
ng-if="filterEditor.isEditingQueryDsl"
|
||||
i18n-id="common.ui.filterEditor.searchFilterValuesLabel"
|
||||
i18n-default-message="Search filter values"
|
||||
></span>
|
||||
|
||||
<span
|
||||
ng-if="!filterEditor.isEditingQueryDsl"
|
||||
i18n-id="common.ui.filterEditor.editQueryDSLLabel"
|
||||
i18n-default-message="Edit Query DSL"
|
||||
></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter dropdowns -->
|
||||
<div
|
||||
class="kuiFieldGroup kuiVerticalRhythmSmall kuiFieldGroup--alignTop"
|
||||
ng-show="!filterEditor.isQueryDslEditorVisible()"
|
||||
>
|
||||
<div class="kuiFieldGroupSection filterEditor__wideField">
|
||||
<filter-field-select
|
||||
data-test-subj="filterfieldSuggestionList"
|
||||
index-patterns="filterEditor.indexPatterns"
|
||||
field="filterEditor.field"
|
||||
on-select="filterEditor.onFieldSelect(field)"
|
||||
></filter-field-select>
|
||||
</div>
|
||||
|
||||
<div class="kuiFieldGroupSection">
|
||||
<filter-operator-select
|
||||
data-test-subj="filterOperatorList"
|
||||
ng-if="filterEditor.field"
|
||||
field="filterEditor.field"
|
||||
operator="filterEditor.operator"
|
||||
on-select="filterEditor.onOperatorSelect(operator)"
|
||||
></filter-operator-select>
|
||||
</div>
|
||||
|
||||
<div class="kuiFieldGroupSection kuiFieldGroupSection--wide filterEditor__wideField filterEditorParamsInput">
|
||||
<filter-params-editor
|
||||
data-test-subj="filterParams"
|
||||
ng-if="filterEditor.field && filterEditor.operator"
|
||||
field="filterEditor.field"
|
||||
operator="filterEditor.operator"
|
||||
params="filterEditor.params"
|
||||
></filter-params-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DSL editor -->
|
||||
<div
|
||||
class="kuiVerticalRhythmSmall"
|
||||
ng-show="filterEditor.isQueryDslEditorVisible()"
|
||||
>
|
||||
<filter-query-dsl-editor
|
||||
class="kuiVerticalRhythmSmall"
|
||||
filter="filterEditor.filter"
|
||||
on-change="filterEditor.setQueryDsl(queryDsl)"
|
||||
is-visible="filterEditor.isQueryDslEditorVisible()"
|
||||
></filter-query-dsl-editor>
|
||||
|
||||
<p class="kuiText kuiVerticalRhythmSmall">
|
||||
<span
|
||||
i18n-id="common.ui.filterEditor.filtersAreBuiltUsingESQueryDSLDescription"
|
||||
i18n-default-message="Filters are built using the "
|
||||
description="Part of composite text: common.ui.filterEditor.filtersAreBuiltUsingESQueryDSLDescription + common.ui.filterEditor.filtersAreBuiltUsingESQueryDSLDescription.esQueryDSLLinkText"
|
||||
></span>
|
||||
<a
|
||||
class="kuiLink"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
documentation-href="query.queryDsl"
|
||||
i18n-id="common.ui.filterEditor.filtersAreBuiltUsingESQueryDSLDescription.esQueryDSLLinkText"
|
||||
i18n-default-message="Elasticsearch Query DSL"
|
||||
description="Part of composite text: common.ui.filterEditor.filtersAreBuiltUsingESQueryDSLDescription + common.ui.filterEditor.filtersAreBuiltUsingESQueryDSLDescription.esQueryDSLLinkText"
|
||||
></a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Label -->
|
||||
<div class="kuiVerticalRhythm">
|
||||
<div class="kuiVerticalRhythmSmall">
|
||||
<label
|
||||
class="kuiLabel"
|
||||
for="filterEditorLabelInput"
|
||||
i18n-id="common.ui.filterEditor.labelLabel"
|
||||
i18n-default-message="Label"
|
||||
></label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="kuiTextInput kuiVerticalRhythmSmall"
|
||||
placeholder="{{ ::'common.ui.filterEditor.optionalPlaceholder' | i18n: { defaultMessage: 'Optional' } }}"
|
||||
type="text"
|
||||
ng-model="filterEditor.alias"
|
||||
id="filterEditorLabelInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="kuiModalFooter kuiBar">
|
||||
<div class="kuiBarSection" ng-hide="filterEditor.filter.meta.isNew">
|
||||
<button
|
||||
class="kuiButton kuiButton--danger"
|
||||
ng-click="filterEditor.onDelete()"
|
||||
aria-label="{{ ::'common.ui.filterEditor.deleteFilterAriaLabel' | i18n: { defaultMessage: 'Delete filter' } }}"
|
||||
>
|
||||
<span
|
||||
class="kuiIcon fa-trash"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="kuiBarSection">
|
||||
<button
|
||||
class="kuiButton kuiButton--basic"
|
||||
ng-click="filterEditor.onCancel()"
|
||||
i18n-id="common.ui.filterEditor.cancelButtonLabel"
|
||||
i18n-default-message="Cancel"
|
||||
></button>
|
||||
<button
|
||||
class="kuiButton kuiButton--primary"
|
||||
ng-disabled="!filterEditor.isValid()"
|
||||
ng-click="filterEditor.save()"
|
||||
data-test-subj="saveFilter"
|
||||
i18n-id="common.ui.filterEditor.saveButtonLabel"
|
||||
i18n-default-message="Save"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<ui-select
|
||||
ng-model="field"
|
||||
ui-select-focus-on="focus-field"
|
||||
on-select="onSelect({ field: field })"
|
||||
uis-open-close="resetLimit()"
|
||||
>
|
||||
<ui-select-match placeholder="{{ ::'common.ui.filterEditor.fieldsPlaceholder' | i18n: { defaultMessage: 'Fields…' } }}">
|
||||
<span
|
||||
class="uiSelectMatch--ellipsis"
|
||||
tooltip="{{$select.selected.name}}"
|
||||
>
|
||||
{{$select.selected.name}}
|
||||
</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices
|
||||
class="uiSelectChoices--autoWidth"
|
||||
repeat="field in fieldOptions
|
||||
| filter:{ name: $select.search }
|
||||
| orderBy:'name'
|
||||
| sortPrefixFirst:$select.search:'name'
|
||||
| limitTo: limit"
|
||||
group-by="getFieldIndexPattern"
|
||||
kbn-scroll-bottom="increaseLimit()"
|
||||
>
|
||||
<div ng-bind-html="field.name | highlight: $select.search"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
<ui-select
|
||||
ng-model="operator"
|
||||
ui-select-focus-on="focus-operator"
|
||||
on-select="onSelect({ operator: operator })"
|
||||
>
|
||||
<ui-select-match placeholder="{{ ::'common.ui.filterEditor.operatorsPlaceholder' | i18n: { defaultMessage: 'Operators…' } }}">
|
||||
{{$select.selected.name}}
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="operator in operatorOptions | filter:{ name: $select.search }">
|
||||
<div ng-bind-html="operator.name | highlight: $select.search"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
<div
|
||||
json-input
|
||||
require-keys="true"
|
||||
kbn-ui-ace-keyboard-mode
|
||||
ui-ace="{
|
||||
mode: 'json',
|
||||
onLoad: aceLoaded
|
||||
}"
|
||||
ng-model="queryDsl"
|
||||
ng-change="onChange({
|
||||
queryDsl: queryDsl
|
||||
})"
|
||||
></div>
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
}
|
|
@ -1,90 +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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const FILTER_OPERATORS = [
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.isLabel', {
|
||||
defaultMessage: 'is'
|
||||
}),
|
||||
type: 'phrase',
|
||||
negate: false,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.isNotLabel', {
|
||||
defaultMessage: 'is not'
|
||||
}),
|
||||
type: 'phrase',
|
||||
negate: true,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.isOneOfLabel', {
|
||||
defaultMessage: 'is one of'
|
||||
}),
|
||||
type: 'phrases',
|
||||
negate: false,
|
||||
fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape']
|
||||
},
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.isNotOneOfLabel', {
|
||||
defaultMessage: 'is not one of'
|
||||
}),
|
||||
type: 'phrases',
|
||||
negate: true,
|
||||
fieldTypes: ['string', 'number', 'date', 'ip', 'geo_point', 'geo_shape']
|
||||
},
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.isBetweenLabel', {
|
||||
defaultMessage: 'is between'
|
||||
}),
|
||||
type: 'range',
|
||||
negate: false,
|
||||
fieldTypes: ['number', 'date', 'ip'],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.isNotBetweenLabel', {
|
||||
defaultMessage: 'is not between'
|
||||
}),
|
||||
type: 'range',
|
||||
negate: true,
|
||||
fieldTypes: ['number', 'date', 'ip'],
|
||||
},
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.existsLabel', {
|
||||
defaultMessage: 'exists'
|
||||
}),
|
||||
type: 'exists',
|
||||
negate: false,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('common.ui.filterEditor.operators.doesNotExistLabel', {
|
||||
defaultMessage: 'does not exist'
|
||||
}),
|
||||
type: 'exists',
|
||||
negate: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const FILTER_OPERATOR_TYPES = _(FILTER_OPERATORS)
|
||||
.map('type')
|
||||
.uniq()
|
||||
.value();
|
|
@ -1,19 +0,0 @@
|
|||
<ng-switch on="operator.type">
|
||||
<filter-params-phrase-editor
|
||||
ng-switch-when="phrase"
|
||||
field="field"
|
||||
params="params"
|
||||
></filter-params-phrase-editor>
|
||||
|
||||
<filter-params-phrases-editor
|
||||
ng-switch-when="phrases"
|
||||
field="field"
|
||||
params="params"
|
||||
></filter-params-phrases-editor>
|
||||
|
||||
<filter-params-range-editor
|
||||
ng-switch-when="range"
|
||||
field="field"
|
||||
params="params"
|
||||
></filter-params-range-editor>
|
||||
</ng-switch>
|
|
@ -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: '='
|
||||
}
|
||||
};
|
||||
});
|
|
@ -1,50 +0,0 @@
|
|||
<div
|
||||
ng-switch="type"
|
||||
class="kuiFieldGroupSection kuiFieldGroupSection--wide"
|
||||
>
|
||||
<input
|
||||
ng-switch-when="string"
|
||||
type="text"
|
||||
placeholder="{{placeholder}}"
|
||||
class="kuiTextInput"
|
||||
ng-model="value"
|
||||
ng-change="onChange({ value: value })"
|
||||
/>
|
||||
<input
|
||||
ng-switch-when="number"
|
||||
string-to-number
|
||||
type="number"
|
||||
placeholder="{{placeholder}}"
|
||||
step="any"
|
||||
class="kuiTextInput"
|
||||
ng-model="value"
|
||||
ng-change="onChange({ value: value })"
|
||||
/>
|
||||
<input
|
||||
ng-switch-when="date"
|
||||
type="text"
|
||||
placeholder="{{placeholder}}"
|
||||
validate-date-math
|
||||
class="kuiTextInput"
|
||||
ng-model="value"
|
||||
ng-change="onChange({ value: value })"
|
||||
/>
|
||||
<input
|
||||
ng-switch-when="ip"
|
||||
type="text"
|
||||
placeholder="{{placeholder}}"
|
||||
validate-ip
|
||||
class="kuiTextInput"
|
||||
ng-model="value"
|
||||
ng-change="onChange({ value: value })"
|
||||
/>
|
||||
<div ng-switch-when="boolean">
|
||||
<select
|
||||
class="kuiSelect"
|
||||
ng-model="value"
|
||||
ng-options="option for option in boolOptions"
|
||||
ng-change="onChange({ value: value })"
|
||||
ng-init="setDefaultBool()"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
|
@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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}`;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
<ui-select
|
||||
ng-if="filterParamsPhraseEditor.shouldSuggestValues && field.aggregatable && field.type === 'string'"
|
||||
ng-model="params.phrase"
|
||||
ui-select-focus-on="focus-params"
|
||||
spinner-enabled="true"
|
||||
spinner-class="kuiIcon kuiIcon--basic fa-spinner fa-spin"
|
||||
>
|
||||
<ui-select-match placeholder="{{ ::'common.ui.filterEditor.filterParamsPhraseEditor.valuesPlaceholder' | i18n: { defaultMessage: 'Values…' } }}">
|
||||
<span
|
||||
class="uiSelectMatch--ellipsis"
|
||||
tooltip="{{$select.selected}}"
|
||||
>
|
||||
{{$select.selected}}
|
||||
</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices
|
||||
class="uiSelectChoices--autoWidth"
|
||||
repeat="value in filterParamsPhraseEditor.compactUnion([$select.search, $select.selected], valueSuggestions) | filter:$select.search | sortPrefixFirst:$select.search"
|
||||
refresh="filterParamsPhraseEditor.refreshValueSuggestions($select.search)"
|
||||
refresh-delay="500"
|
||||
>
|
||||
<div ng-bind="value"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
|
||||
<filter-params-input-type
|
||||
ng-if="!(filterParamsPhraseEditor.shouldSuggestValues && field.aggregatable && field.type === 'string')"
|
||||
placeholder="{{ ::'common.ui.filterEditor.filterParamsPhraseEditor.valuePlaceholder' | i18n: { defaultMessage: 'Value…' } }}"
|
||||
type="field.type"
|
||||
value="params.phrase"
|
||||
on-change="params.phrase = value"
|
||||
focus-on="focus-params"
|
||||
></filter-params-input-type>
|
||||
|
||||
<small>
|
||||
<a
|
||||
ng-if="field.type === 'date'"
|
||||
class="kuiLink"
|
||||
documentation-href="date.dateMath"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
i18n-id="common.ui.filterEditor.filterParamsPhraseEditor.acceptedDateFormatsLinkText"
|
||||
i18n-default-message="Accepted date formats"
|
||||
></a>
|
||||
</small>
|
|
@ -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
|
||||
};
|
||||
});
|
|
@ -1,28 +0,0 @@
|
|||
<ui-select
|
||||
ng-model="params.phrases"
|
||||
multiple
|
||||
ui-select-focus-on="focus-params"
|
||||
spinner-enabled="true"
|
||||
spinner-class="kuiIcon kuiIcon--basic fa-spinner fa-spin"
|
||||
>
|
||||
<ui-select-match
|
||||
placeholder="{{ ::'common.ui.filterEditor.filterParamsPhrasesEditor.valuesPlaceholder' | i18n: { defaultMessage: 'Values…' } }}"
|
||||
class="uiSelectMatch--restrictToParent"
|
||||
>
|
||||
<span
|
||||
data-test-subj="filterEditorPhrases"
|
||||
class="uiSelectMatch--pillWithTooltip"
|
||||
tooltip="{{$item}}"
|
||||
>
|
||||
{{$item}}
|
||||
</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices
|
||||
class="uiSelectChoices--autoWidth"
|
||||
repeat="value in filterParamsPhrasesEditor.compactUnion([$select.search], valueSuggestions) | filter:$select.search | sortPrefixFirst:$select.search"
|
||||
refresh="filterParamsPhrasesEditor.refreshValueSuggestions($select.search)"
|
||||
refresh-delay="500"
|
||||
>
|
||||
<div ng-bind-html="value | highlight: $select.search"></div>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
|
@ -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
|
||||
};
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
<div class="kuiVerticalRhythm">
|
||||
<filter-params-input-type
|
||||
placeholder="{{ ::'common.ui.filterEditor.filterParamsRangeEditor.fromPlaceholder' | i18n: { defaultMessage: 'From…' } }}"
|
||||
type="field.type"
|
||||
value="params.range.from"
|
||||
on-change="params.range.from = value"
|
||||
focus-on="focus-params"
|
||||
></filter-params-input-type>
|
||||
</div>
|
||||
|
||||
<div class="kuiVerticalRhythm">
|
||||
<filter-params-input-type
|
||||
placeholder="{{ ::'common.ui.filterEditor.filterParamsRangeEditor.toPlaceholder' | i18n: { defaultMessage: 'To…' } }}"
|
||||
type="field.type"
|
||||
value="params.range.to"
|
||||
on-change="params.range.to = value"
|
||||
></filter-params-input-type>
|
||||
</div>
|
||||
|
||||
<small>
|
||||
<a
|
||||
ng-if="field.type === 'date'"
|
||||
class="kuiLink"
|
||||
documentation-href="date.dateMath"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
i18n-id="common.ui.filterEditor.filterParamsRangeEditor.acceptedDateFormatsLinkText"
|
||||
i18n-default-message="Accepted date formats"
|
||||
></a>
|
||||
</small>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue