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:
Matt Bargar 2019-01-31 18:36:26 -05:00 committed by GitHub
parent f67e89b190
commit 8a83955650
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
139 changed files with 4299 additions and 3582 deletions

View 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.

View file

@ -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"
}
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
import './filter_editor';
require('../tasks/build_cli');

View file

@ -17,6 +17,7 @@
* under the License.
*/
// Creates a filter where the given field exists
export function buildExistsFilter(field, indexPattern) {
return {
meta: {

View file

@ -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;

View file

@ -22,3 +22,4 @@ export * from './phrase';
export * from './phrases';
export * from './query';
export * from './range';
export * from './lib';

View file

@ -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;
};

View 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;
};

View file

@ -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;
};

View 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;
};

View 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;

View 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);
}

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View file

@ -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);

View file

@ -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';

View file

@ -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,

View file

@ -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;

View file

@ -18,3 +18,4 @@
*/
export * from './kuery';
export * from './filters';

View 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);
});

View file

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.browser.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./target/public"
},
"include": [
"index.d.ts",
"src/**/*.ts"
]
}

View file

@ -1,7 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./target/server"
},
"include": [
"index.d.ts",
"src/**/*.d.ts"
"src/**/*.ts"
]
}

View file

@ -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'
}
};

View file

@ -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

View file

@ -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';

View file

@ -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,

View file

@ -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()"

View file

@ -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();

View file

@ -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();
});

View file

@ -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>

View file

@ -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()"

View file

@ -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();
});

View file

@ -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"

View file

@ -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(() => {

View 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);
};
}

View 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>

View 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(),
};
});
});
}
};
});

View file

@ -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';

View file

@ -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');

View 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;

View file

@ -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);
});
});
});
});
});

View file

@ -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);
});
});
});

View 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;
}

View 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;
}

View file

@ -0,0 +1,2 @@
@import 'global_filter_group';
@import 'global_filter_item';

View file

@ -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));
});

View file

@ -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>

View file

@ -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();
}
};
});

View file

@ -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;
}

View 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);

View file

@ -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;
}
};
};
}

View 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}
/>
);
}

View 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);

View file

@ -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);
});
});
});

View file

@ -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;
}

View 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,
];

View file

@ -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'
}
};

View file

@ -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,
},
};

View file

@ -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,
},
};

View file

@ -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,
},
};

View 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 });
}
}

View file

@ -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);

View file

@ -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);

View 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);

View 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);

View 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);

View file

@ -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>

View file

@ -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' }),
};
}
};
});

View 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)}`;
}
}

View 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';

View file

@ -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);
});
});

View file

@ -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]);
});
});
});

View file

@ -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();
});
});
});

View file

@ -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]);
});
});
});
}

View file

@ -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';
}

View file

@ -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

View file

@ -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>

View file

@ -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());
}
});
})
};
});

View file

@ -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;
}

View file

@ -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>

View file

@ -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();
}
};
});

View file

@ -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>

View file

@ -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);
});
}
};
});

View file

@ -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>

View file

@ -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();
}
});
}
}
};
});

View file

@ -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);
});
});
});

View file

@ -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';
}

View file

@ -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();

View file

@ -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>

View file

@ -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: '='
}
};
});

View file

@ -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>

View file

@ -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
});
}
};
}
};
});

View file

@ -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}`;
}
}

View file

@ -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>

View file

@ -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
};
});

View file

@ -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>

View file

@ -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
};
});

View file

@ -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