-
+
diff --git a/src/core_plugins/kibana/public/visualize/editor/editor.html b/src/core_plugins/kibana/public/visualize/editor/editor.html
index 1eaf911da377f2..64030eec78d8b3 100644
--- a/src/core_plugins/kibana/public/visualize/editor/editor.html
+++ b/src/core_plugins/kibana/public/visualize/editor/editor.html
@@ -80,7 +80,10 @@
-
+
{
+ const suggestions = res.aggregations.suggestions.buckets.map(bucket => bucket.key);
+ return reply(suggestions);
+ })
+ .catch(error => reply(handleESError(error)));
+ }
+ });
+}
+
+function getBody(terms) {
+ return {
+ aggs: {
+ suggestions: { terms }
+ }
+ };
+}
diff --git a/src/fixtures/filters/exists_filter.js b/src/fixtures/filters/exists_filter.js
new file mode 100644
index 00000000000000..f1e2e77fc342b8
--- /dev/null
+++ b/src/fixtures/filters/exists_filter.js
@@ -0,0 +1,16 @@
+export const existsFilter = {
+ 'meta': {
+ 'index': 'logstash-*',
+ 'negate': false,
+ 'disabled': false,
+ 'type': 'exists',
+ 'key': 'machine.os',
+ 'value': 'exists'
+ },
+ 'exists': {
+ 'field': 'machine.os'
+ },
+ '$state': {
+ 'store': 'appState'
+ }
+};
diff --git a/src/fixtures/filters/index.js b/src/fixtures/filters/index.js
new file mode 100644
index 00000000000000..ddc3303ad4683a
--- /dev/null
+++ b/src/fixtures/filters/index.js
@@ -0,0 +1,5 @@
+export { phraseFilter } from './phrase_filter';
+export { scriptedPhraseFilter } from './scripted_phrase_filter';
+export { phrasesFilter } from './phrases_filter';
+export { rangeFilter } from './range_filter';
+export { existsFilter } from './exists_filter';
diff --git a/src/fixtures/filters/phrase_filter.js b/src/fixtures/filters/phrase_filter.js
new file mode 100644
index 00000000000000..3beb6a48ae5cee
--- /dev/null
+++ b/src/fixtures/filters/phrase_filter.js
@@ -0,0 +1,21 @@
+export const phraseFilter = {
+ meta: {
+ negate: false,
+ index: 'logstash-*',
+ type: 'phrase',
+ key: 'machine.os',
+ value: 'ios',
+ disabled: false
+ },
+ query: {
+ match: {
+ 'machine.os': {
+ query: 'ios',
+ type: 'phrase'
+ }
+ }
+ },
+ $state: {
+ store: 'appState'
+ }
+};
diff --git a/src/fixtures/filters/phrases_filter.js b/src/fixtures/filters/phrases_filter.js
new file mode 100644
index 00000000000000..ee263fff1776ef
--- /dev/null
+++ b/src/fixtures/filters/phrases_filter.js
@@ -0,0 +1,34 @@
+export const phrasesFilter = {
+ meta: {
+ index: 'logstash-*',
+ type: 'phrases',
+ key: 'machine.os.raw',
+ value: '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
+ }
+ },
+ $state: {
+ store: 'appState'
+ }
+};
diff --git a/src/fixtures/filters/range_filter.js b/src/fixtures/filters/range_filter.js
new file mode 100644
index 00000000000000..f0fd90ac4d58b4
--- /dev/null
+++ b/src/fixtures/filters/range_filter.js
@@ -0,0 +1,20 @@
+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'
+ }
+};
diff --git a/src/fixtures/filters/scripted_phrase_filter.js b/src/fixtures/filters/scripted_phrase_filter.js
new file mode 100644
index 00000000000000..bb243a1435bec0
--- /dev/null
+++ b/src/fixtures/filters/scripted_phrase_filter.js
@@ -0,0 +1,23 @@
+export const scriptedPhraseFilter = {
+ 'meta': {
+ 'negate': false,
+ 'index': 'logstash-*',
+ 'field': 'script string',
+ 'type': 'phrase',
+ 'key': 'script string',
+ 'value': 'i am a string',
+ 'disabled': false
+ },
+ 'script': {
+ 'script': {
+ 'inline': 'boolean compare(Supplier s, def v) {return s.get() == v;}compare(() -> { \'i am a string\' }, params.value);',
+ 'lang': 'painless',
+ 'params': {
+ 'value': 'i am a string'
+ }
+ }
+ },
+ '$state': {
+ 'store': 'appState'
+ }
+};
diff --git a/src/ui/public/agg_types/controls/date_ranges.html b/src/ui/public/agg_types/controls/date_ranges.html
index d007e35a8a70d8..9bf071bc2ef483 100644
--- a/src/ui/public/agg_types/controls/date_ranges.html
+++ b/src/ui/public/agg_types/controls/date_ranges.html
@@ -38,7 +38,13 @@
- Accepted Date Formats
+
+ Accepted date formats
+
diff --git a/src/ui/public/agg_types/index.js b/src/ui/public/agg_types/index.js
index 8fbaf2200b1f8e..2e7a9d55dd91b0 100644
--- a/src/ui/public/agg_types/index.js
+++ b/src/ui/public/agg_types/index.js
@@ -99,4 +99,3 @@ export function AggTypesIndexProvider(Private) {
initialSet: aggs.metrics.concat(aggs.buckets)
});
}
-
diff --git a/src/ui/public/directives/focus_on.js b/src/ui/public/directives/focus_on.js
new file mode 100644
index 00000000000000..024644a3e59777
--- /dev/null
+++ b/src/ui/public/directives/focus_on.js
@@ -0,0 +1,11 @@
+import { uiModules } from 'ui/modules';
+const module = uiModules.get('kibana');
+
+module.directive('focusOn', ($timeout) => ({
+ restrict: 'A',
+ link: function (scope, elem, attrs) {
+ scope.$on(attrs.focusOn, () => {
+ $timeout(() => elem.find('input').addBack('input').focus());
+ });
+ }
+}));
diff --git a/src/ui/public/directives/ui_select_focus_on.js b/src/ui/public/directives/ui_select_focus_on.js
new file mode 100644
index 00000000000000..61bc428ecfc040
--- /dev/null
+++ b/src/ui/public/directives/ui_select_focus_on.js
@@ -0,0 +1,12 @@
+import { uiModules } from 'ui/modules';
+const module = uiModules.get('kibana');
+
+module.directive('uiSelectFocusOn', ($timeout) => ({
+ restrict: 'A',
+ require: 'uiSelect',
+ link: function (scope, elem, attrs, uiSelect) {
+ scope.$on(attrs.uiSelectFocusOn, () => {
+ $timeout(() => uiSelect.activate());
+ });
+ }
+}));
diff --git a/src/ui/public/documentation_links/documentation_links.js b/src/ui/public/documentation_links/documentation_links.js
index 1a7ffc973fd955..cec22d65b9cbed 100644
--- a/src/ui/public/documentation_links/documentation_links.js
+++ b/src/ui/public/documentation_links/documentation_links.js
@@ -35,6 +35,9 @@ export const documentationLinks = {
query: {
luceneQuerySyntax: `${baseUrl}guide/en/elasticsearch/reference/${urlVersion}/query-dsl-query-string-query.html#query-string-syntax`
},
+ date: {
+ dateMath: `${baseUrl}guide/en/elasticsearch/reference/${urlVersion}/common-options.html#date-math`
+ },
demoSite: 'http://demo.elastic.co',
gettingStarted: `${baseUrl}products/kibana/getting-started-link`
};
diff --git a/src/ui/public/filter_bar/__tests__/_update_filters.js b/src/ui/public/filter_bar/__tests__/_update_filters.js
deleted file mode 100644
index 6634d2f8ff8a66..00000000000000
--- a/src/ui/public/filter_bar/__tests__/_update_filters.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import _ from 'lodash';
-import expect from 'expect.js';
-import ngMock from 'ng_mock';
-import MockState from 'fixtures/mock_state';
-import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
-
-describe('update filters', function () {
- let queryFilter;
- let appState;
- let globalState;
- let $rootScope;
-
- beforeEach(ngMock.module(
- 'kibana',
- 'kibana/courier',
- 'kibana/global_state',
- function ($provide) {
- $provide.service('courier', require('fixtures/mock_courier'));
-
- appState = new MockState({ filters: [] });
- $provide.service('getAppState', function () {
- return function () { return appState; };
- });
-
- globalState = new MockState({ filters: [] });
- $provide.service('globalState', function () {
- return globalState;
- });
- }
- ));
-
- beforeEach(ngMock.inject(function (Private, _$rootScope_) {
- $rootScope = _$rootScope_;
- queryFilter = Private(FilterBarQueryFilterProvider);
- }));
-
- describe('updating', function () {
- let currentFilter;
- let newFilter;
-
- beforeEach(function () {
- newFilter = _.cloneDeep({
- query: {
- match: {
- extension: {
- query: 'jpg',
- type: 'phrase'
- }
- }
- }
- });
- currentFilter = _.assign(_.cloneDeep(newFilter), {
- meta: {}
- });
- });
-
- it('should be able to update a filter', function () {
- newFilter.query.match.extension.query = 'png';
-
- expect(currentFilter.query.match.extension.query).to.be('jpg');
- queryFilter.updateFilter({
- source: currentFilter,
- model: newFilter,
- type: 'query'
- });
- $rootScope.$digest();
- expect(currentFilter.query.match.extension.query).to.be('png');
- });
-
- it('should set an alias in the meta object', function () {
-
- queryFilter.updateFilter({
- source: currentFilter,
- model: newFilter,
- type: 'query',
- alias: 'foo'
- });
- $rootScope.$digest();
- expect(currentFilter.meta.alias).to.be('foo');
- });
-
- it('should replace the filter type if it is changed', function () {
- newFilter = {
- 'range': {
- 'bytes': {
- 'gte': 0,
- 'lt': 1000
- }
- }
- };
-
- expect(currentFilter.query).not.to.be(undefined);
-
- queryFilter.updateFilter({
- source: currentFilter,
- model: newFilter,
- type: 'query'
- });
- $rootScope.$digest();
-
- expect(currentFilter.query).to.be(undefined);
- expect(currentFilter.range).not.to.be(undefined);
- expect(_.eq(currentFilter.range, newFilter.range)).to.be(true);
- });
-
- });
-});
diff --git a/src/ui/public/filter_bar/__tests__/filter_bar.js b/src/ui/public/filter_bar/__tests__/filter_bar.js
index f90c533d739275..509ff5f10e9b91 100644
--- a/src/ui/public/filter_bar/__tests__/filter_bar.js
+++ b/src/ui/public/filter_bar/__tests__/filter_bar.js
@@ -79,10 +79,10 @@ describe('Filter Bar Directive', function () {
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('exists:');
- expect($(filters[2]).find('span')[1].innerHTML).to.equal('"@timestamp"');
- expect($(filters[3]).find('span')[0].innerHTML).to.equal('missing:');
- expect($(filters[3]).find('span')[1].innerHTML).to.equal('"host"');
+ 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 () {
@@ -92,7 +92,7 @@ describe('Filter Bar Directive', function () {
describe('editing filters', function () {
beforeEach(function () {
- $scope.startEditingFilter(appState.filters[3]);
+ $scope.editFilter(appState.filters[3]);
$scope.$digest();
});
@@ -101,16 +101,18 @@ describe('Filter Bar Directive', function () {
});
it('should be able to stop editing a filter', function () {
- $scope.stopEditingFilter();
+ $scope.cancelEdit();
$scope.$digest();
expect($el.find('.filter-edit-container').length).to.be(0);
});
- it('should merge changes after clicking done', function () {
- sinon.spy($scope, 'updateFilter');
+ it('should remove old filter and add new filter when saving', function () {
+ sinon.spy($scope, 'removeFilter');
+ sinon.spy($scope, 'addFilters');
- $scope.editDone();
- expect($scope.updateFilter.called).to.be(true);
+ $scope.saveEdit(appState.filters[3], appState.filters[3], false);
+ expect($scope.removeFilter.called).to.be(true);
+ expect($scope.addFilters.called).to.be(true);
});
});
});
diff --git a/src/ui/public/filter_bar/__tests__/query_filter.js b/src/ui/public/filter_bar/__tests__/query_filter.js
index 4b7bf78fec1533..c3d1f0a3234b7c 100644
--- a/src/ui/public/filter_bar/__tests__/query_filter.js
+++ b/src/ui/public/filter_bar/__tests__/query_filter.js
@@ -3,7 +3,6 @@ import ngMock from 'ng_mock';
import './_get_filters';
import './_add_filters';
import './_remove_filters';
-import './_update_filters';
import './_toggle_filters';
import './_invert_filters';
import './_pin_filters';
@@ -39,7 +38,6 @@ describe('Query Filter', function () {
expect(queryFilter.toggleAll).to.be.a('function');
expect(queryFilter.removeFilter).to.be.a('function');
expect(queryFilter.removeAll).to.be.a('function');
- expect(queryFilter.updateFilter).to.be.a('function');
expect(queryFilter.invertFilter).to.be.a('function');
expect(queryFilter.invertAll).to.be.a('function');
expect(queryFilter.pinFilter).to.be.a('function');
diff --git a/src/ui/public/filter_bar/filter_bar.html b/src/ui/public/filter_bar/filter_bar.html
index 4598d28cddba40..29a60c192a049a 100644
--- a/src/ui/public/filter_bar/filter_bar.html
+++ b/src/ui/public/filter_bar/filter_bar.html
@@ -22,7 +22,10 @@
-
+
-
-
+
+
-
+
+
+
-
+
+
+
-
+
+
+ {{$select.selected.name}}
+
+
+
+
+
+
diff --git a/src/ui/public/filter_editor/filter_field_select.js b/src/ui/public/filter_editor/filter_field_select.js
new file mode 100644
index 00000000000000..529e895f5e3719
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_field_select.js
@@ -0,0 +1,24 @@
+import 'angular-ui-select';
+import { uiModules } from 'ui/modules';
+import { getFieldOptions } from './lib/filter_editor_utils';
+import template from './filter_field_select.html';
+import '../directives/ui_select_focus_on';
+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 = getFieldOptions(indexPatterns);
+ });
+ }
+ };
+});
diff --git a/src/ui/public/filter_editor/filter_operator_select.html b/src/ui/public/filter_editor/filter_operator_select.html
new file mode 100644
index 00000000000000..78b730b93b58aa
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_operator_select.html
@@ -0,0 +1,12 @@
+
+
+ {{$select.selected.name}}
+
+
+
+
+
diff --git a/src/ui/public/filter_editor/filter_operator_select.js b/src/ui/public/filter_editor/filter_operator_select.js
new file mode 100644
index 00000000000000..d8ed0ecd601499
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_operator_select.js
@@ -0,0 +1,23 @@
+import 'angular-ui-select';
+import { uiModules } from 'ui/modules';
+import { getOperatorOptions } from './lib/filter_editor_utils';
+import template from './filter_operator_select.html';
+import '../directives/ui_select_focus_on';
+
+const module = uiModules.get('kibana');
+module.directive('filterOperatorSelect', function () {
+ return {
+ restrict: 'E',
+ template,
+ scope: {
+ field: '=',
+ operator: '=',
+ onSelect: '&'
+ },
+ link: function ($scope) {
+ $scope.$watch('field', (field) => {
+ $scope.operatorOptions = getOperatorOptions(field);
+ });
+ }
+ };
+});
diff --git a/src/ui/public/filter_editor/filter_query_dsl_editor.html b/src/ui/public/filter_editor/filter_query_dsl_editor.html
new file mode 100644
index 00000000000000..41d73b06eb2a6b
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_query_dsl_editor.html
@@ -0,0 +1,12 @@
+
diff --git a/src/ui/public/filter_editor/filter_query_dsl_editor.js b/src/ui/public/filter_editor/filter_query_dsl_editor.js
new file mode 100644
index 00000000000000..b724ecb53a2022
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_query_dsl_editor.js
@@ -0,0 +1,27 @@
+import 'ace';
+import _ from 'lodash';
+import { uiModules } from 'ui/modules';
+import template from './filter_query_dsl_editor.html';
+
+const module = uiModules.get('kibana');
+module.directive('filterQueryDslEditor', function () {
+ return {
+ restrict: 'E',
+ template,
+ scope: {
+ filter: '=',
+ onChange: '&'
+ },
+ link: {
+ pre: function ($scope) {
+ $scope.queryDsl = _.omit($scope.filter, ['meta', '$state']);
+ $scope.aceLoaded = function (editor) {
+ editor.$blockScrolling = Infinity;
+ const session = editor.getSession();
+ session.setTabSize(2);
+ session.setUseSoftTabs(true);
+ };
+ }
+ }
+ };
+});
diff --git a/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js b/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js
new file mode 100644
index 00000000000000..f91b16f40f8769
--- /dev/null
+++ b/src/ui/public/filter_editor/lib/__tests__/filter_editor_utils.js
@@ -0,0 +1,342 @@
+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,
+ getFieldOptions,
+ getOperatorOptions,
+ isFilterValid,
+ buildFilter
+} 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('getFieldOptions', function () {
+ it('returns an empty array when no index patterns are provided', function () {
+ const fieldOptions = getFieldOptions();
+ expect(fieldOptions).to.eql([]);
+ });
+
+ it('returns the list of fields from the given index patterns', function () {
+ const fieldOptions = getFieldOptions([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 = getFieldOptions([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();
+ });
+ });
+});
diff --git a/src/ui/public/filter_editor/lib/filter_editor_utils.js b/src/ui/public/filter_editor/lib/filter_editor_utils.js
new file mode 100644
index 00000000000000..f6d5f8e6e7154f
--- /dev/null
+++ b/src/ui/public/filter_editor/lib/filter_editor_utils.js
@@ -0,0 +1,83 @@
+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 getFieldOptions(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;
+}
diff --git a/src/ui/public/filter_editor/lib/filter_operators.js b/src/ui/public/filter_editor/lib/filter_operators.js
new file mode 100644
index 00000000000000..fe14bbe9c4e3fc
--- /dev/null
+++ b/src/ui/public/filter_editor/lib/filter_operators.js
@@ -0,0 +1,51 @@
+import _ from 'lodash';
+
+export const FILTER_OPERATORS = [
+ {
+ name: 'is',
+ type: 'phrase',
+ negate: false,
+ },
+ {
+ name: 'is not',
+ type: 'phrase',
+ negate: true,
+ },
+ {
+ name: 'is one of',
+ type: 'phrases',
+ negate: false,
+ },
+ {
+ name: 'is not one of',
+ type: 'phrases',
+ negate: true,
+ },
+ {
+ name: 'is between',
+ type: 'range',
+ negate: false,
+ fieldTypes: ['number', 'date', 'ip'],
+ },
+ {
+ name: 'is not between',
+ type: 'range',
+ negate: true,
+ fieldTypes: ['number', 'date', 'ip'],
+ },
+ {
+ name: 'exists',
+ type: 'exists',
+ negate: false,
+ },
+ {
+ name: 'does not exist',
+ type: 'exists',
+ negate: true,
+ },
+];
+
+export const FILTER_OPERATOR_TYPES = _(FILTER_OPERATORS)
+ .map('type')
+ .uniq()
+ .value();
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_editor.html
new file mode 100644
index 00000000000000..f242ce2af5a2f6
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_editor.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_editor.js b/src/ui/public/filter_editor/params_editor/filter_params_editor.js
new file mode 100644
index 00000000000000..fff50d1098c2cb
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_editor.js
@@ -0,0 +1,18 @@
+import { uiModules } from 'ui/modules';
+import template from './filter_params_editor.html';
+import './filter_params_phrase_editor';
+import './filter_params_phrases_editor';
+import './filter_params_range_editor';
+
+const module = uiModules.get('kibana');
+module.directive('filterParamsEditor', function () {
+ return {
+ restrict: 'E',
+ template,
+ scope: {
+ field: '=',
+ operator: '=',
+ params: '='
+ }
+ };
+});
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_input_type.html b/src/ui/public/filter_editor/params_editor/filter_params_input_type.html
new file mode 100644
index 00000000000000..56ffa8068d0d0e
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_input_type.html
@@ -0,0 +1,40 @@
+
+
+
+ {{$select.selected}}
+
+
+
+
+
+
+
+
+
+
+
+ Accepted date formats
+
+
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js
new file mode 100644
index 00000000000000..82cf98200c2120
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.js
@@ -0,0 +1,26 @@
+import 'angular-ui-select';
+import { uiModules } from 'ui/modules';
+import template from './filter_params_phrase_editor.html';
+import { filterParamsPhraseController } from './filter_params_phrase_controller';
+import { documentationLinks } from 'ui/documentation_links/documentation_links';
+import './filter_params_input_type';
+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,
+ link: function (scope) {
+ scope.dateDocLinks = documentationLinks.date;
+ }
+ };
+});
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html
new file mode 100644
index 00000000000000..261fd872dbb5fa
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.html
@@ -0,0 +1,25 @@
+
+
+
+ {{$item}}
+
+
+
+
+
+
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js
new file mode 100644
index 00000000000000..5a87e7c2c3361a
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_phrases_editor.js
@@ -0,0 +1,20 @@
+import 'angular-ui-select';
+import { uiModules } from 'ui/modules';
+import template from './filter_params_phrases_editor.html';
+import { filterParamsPhraseController } from './filter_params_phrase_controller';
+import '../../directives/ui_select_focus_on';
+import '../../filters/sort_prefix_first';
+
+const module = uiModules.get('kibana');
+module.directive('filterParamsPhrasesEditor', function () {
+ return {
+ restrict: 'E',
+ template,
+ scope: {
+ field: '=',
+ params: '='
+ },
+ controllerAs: 'filterParamsPhrasesEditor',
+ controller: filterParamsPhraseController
+ };
+});
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_range_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.html
new file mode 100644
index 00000000000000..2381b68b717aaf
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.html
@@ -0,0 +1,29 @@
+
All filters:
-
-
+
+
+
diff --git a/src/ui/public/filter_bar/filter_bar.js b/src/ui/public/filter_bar/filter_bar.js
index b9aed3f269c6b2..0793768ad73c43 100644
--- a/src/ui/public/filter_bar/filter_bar.js
+++ b/src/ui/public/filter_bar/filter_bar.js
@@ -1,6 +1,7 @@
import _ from 'lodash';
import template from 'ui/filter_bar/filter_bar.html';
import 'ui/directives/json_input';
+import '../filter_editor';
import { filterAppliedAndUnwrap } from 'ui/filter_bar/lib/filter_applied_and_unwrap';
import { FilterBarLibMapAndFlattenFiltersProvider } from 'ui/filter_bar/lib/map_and_flatten_filters';
import { FilterBarLibMapFlattenAndWrapFiltersProvider } from 'ui/filter_bar/lib/map_flatten_and_wrap_filters';
@@ -23,12 +24,13 @@ module.directive('filterBar', function (Private, Promise, getAppState) {
const filterOutTimeBasedFilter = Private(FilterBarLibFilterOutTimeBasedFilterProvider);
const changeTimeFilter = Private(FilterBarLibChangeTimeFilterProvider);
const queryFilter = Private(FilterBarQueryFilterProvider);
- const privateFilterFieldRegex = /(^\$|meta)/;
return {
+ template,
restrict: 'E',
- template: template,
- scope: {},
+ scope: {
+ indexPatterns: '='
+ },
link: function ($scope) {
// bind query filter actions to the scope
[
@@ -40,19 +42,15 @@ module.directive('filterBar', function (Private, Promise, getAppState) {
'invertFilter',
'invertAll',
'removeFilter',
- 'removeAll',
- 'updateFilter'
+ 'removeAll'
].forEach(function (method) {
$scope[method] = queryFilter[method];
});
$scope.state = getAppState();
- $scope.aceLoaded = function (editor) {
- editor.$blockScrolling = Infinity;
- const session = editor.getSession();
- session.setTabSize(2);
- session.setUseSoftTabs(true);
+ $scope.showAddFilterButton = () => {
+ return _.compact($scope.indexPatterns).length > 0;
};
$scope.applyFilters = function (filters) {
@@ -65,24 +63,29 @@ module.directive('filterBar', function (Private, Promise, getAppState) {
}
};
- $scope.startEditingFilter = function (source) {
- return $scope.editingFilter = {
- source: source,
- type: _.findKey(source, function (val, key) {
- return !key.match(privateFilterFieldRegex);
- }),
- model: convertToEditableFilter(source),
- alias: source.meta.alias
+ $scope.addFilter = () => {
+ $scope.editingFilter = {
+ meta: { isNew: true }
};
};
- $scope.stopEditingFilter = function () {
- $scope.editingFilter = null;
+ $scope.deleteFilter = (filter) => {
+ $scope.removeFilter(filter);
+ if (filter === $scope.editingFilter) $scope.cancelEdit();
};
- $scope.editDone = function () {
- $scope.updateFilter($scope.editingFilter);
- $scope.stopEditingFilter();
+ $scope.editFilter = (filter) => {
+ $scope.editingFilter = filter;
+ };
+
+ $scope.cancelEdit = () => {
+ delete $scope.editingFilter;
+ };
+
+ $scope.saveEdit = (filter, newFilter, isPinned) => {
+ if (!filter.isNew) $scope.removeFilter(filter);
+ delete $scope.editingFilter;
+ $scope.addFilters([newFilter], isPinned);
};
$scope.clearFilterBar = function () {
@@ -92,7 +95,6 @@ module.directive('filterBar', function (Private, Promise, getAppState) {
// update the scope filter list on filter changes
$scope.$listen(queryFilter, 'update', function () {
- $scope.stopEditingFilter();
updateFilters();
});
@@ -152,12 +154,6 @@ module.directive('filterBar', function (Private, Promise, getAppState) {
$scope.addFilters(newFilters);
}
- function convertToEditableFilter(filter) {
- return _.omit(_.cloneDeep(filter), function (val, key) {
- return key.match(privateFilterFieldRegex);
- });
- }
-
function updateFilters() {
const filters = queryFilter.getFilters();
mapAndFlattenFilters(filters).then(function (results) {
diff --git a/src/ui/public/filter_bar/filter_bar.less b/src/ui/public/filter_bar/filter_bar.less
index 3326e0ec3d2523..c1fa15519d32fe 100644
--- a/src/ui/public/filter_bar/filter_bar.less
+++ b/src/ui/public/filter_bar/filter_bar.less
@@ -1,5 +1,9 @@
@import (reference) "~ui/styles/variables";
+filter-bar {
+ z-index: 20 !important;
+}
+
.filter-bar-confirm {
padding: 8px 10px 4px;
background: @filter-bar-confirm-bg;
diff --git a/src/ui/public/filter_bar/lib/__tests__/map_and_flatten_filters.js b/src/ui/public/filter_bar/lib/__tests__/map_and_flatten_filters.js
index b50404e9a06c25..b3d832cff3518e 100644
--- a/src/ui/public/filter_bar/lib/__tests__/map_and_flatten_filters.js
+++ b/src/ui/public/filter_bar/lib/__tests__/map_and_flatten_filters.js
@@ -39,10 +39,10 @@ describe('Filter Bar Directive', function () {
expect(results[2]).to.have.property('meta');
expect(results[3]).to.have.property('meta');
expect(results[4]).to.have.property('meta');
- expect(results[0].meta).to.have.property('key', 'exists');
- expect(results[0].meta).to.have.property('value', '_type');
- expect(results[1].meta).to.have.property('key', 'missing');
- expect(results[1].meta).to.have.property('value', '_type');
+ expect(results[0].meta).to.have.property('key', '_type');
+ expect(results[0].meta).to.have.property('value', 'exists');
+ expect(results[1].meta).to.have.property('key', '_type');
+ expect(results[1].meta).to.have.property('value', 'missing');
expect(results[2].meta).to.have.property('key', 'query');
expect(results[2].meta).to.have.property('value', 'foo:bar');
expect(results[3].meta).to.have.property('key', 'bytes');
diff --git a/src/ui/public/filter_bar/lib/__tests__/map_exists.js b/src/ui/public/filter_bar/lib/__tests__/map_exists.js
index e67752d5e28f09..29ff27d0a65f85 100644
--- a/src/ui/public/filter_bar/lib/__tests__/map_exists.js
+++ b/src/ui/public/filter_bar/lib/__tests__/map_exists.js
@@ -16,8 +16,8 @@ describe('Filter Bar Directive', function () {
it('should return the key and value for matching filters', function (done) {
const filter = { exists: { field: '_type' } };
mapExists(filter).then(function (result) {
- expect(result).to.have.property('key', 'exists');
- expect(result).to.have.property('value', '_type');
+ expect(result).to.have.property('key', '_type');
+ expect(result).to.have.property('value', 'exists');
done();
});
$rootScope.$apply();
diff --git a/src/ui/public/filter_bar/lib/__tests__/map_filter.js b/src/ui/public/filter_bar/lib/__tests__/map_filter.js
index 7f46c9446a2620..dffbc6cd4ae58d 100644
--- a/src/ui/public/filter_bar/lib/__tests__/map_filter.js
+++ b/src/ui/public/filter_bar/lib/__tests__/map_filter.js
@@ -38,8 +38,8 @@ describe('Filter Bar Directive', function () {
const before = { meta: { index: 'logstash-*' }, exists: { field: '@timestamp' } };
mapFilter(before).then(function (after) {
expect(after).to.have.property('meta');
- expect(after.meta).to.have.property('key', 'exists');
- expect(after.meta).to.have.property('value', '@timestamp');
+ expect(after.meta).to.have.property('key', '@timestamp');
+ expect(after.meta).to.have.property('value', 'exists');
expect(after.meta).to.have.property('disabled', false);
expect(after.meta).to.have.property('negate', false);
done();
@@ -51,8 +51,8 @@ describe('Filter Bar Directive', function () {
const before = { meta: { index: 'logstash-*' }, missing: { field: '@timestamp' } };
mapFilter(before).then(function (after) {
expect(after).to.have.property('meta');
- expect(after.meta).to.have.property('key', 'missing');
- expect(after.meta).to.have.property('value', '@timestamp');
+ expect(after.meta).to.have.property('key', '@timestamp');
+ expect(after.meta).to.have.property('value', 'missing');
expect(after.meta).to.have.property('disabled', false);
expect(after.meta).to.have.property('negate', false);
done();
diff --git a/src/ui/public/filter_bar/lib/__tests__/map_missing.js b/src/ui/public/filter_bar/lib/__tests__/map_missing.js
index 61f604e4227359..778a800084ec33 100644
--- a/src/ui/public/filter_bar/lib/__tests__/map_missing.js
+++ b/src/ui/public/filter_bar/lib/__tests__/map_missing.js
@@ -17,8 +17,8 @@ describe('Filter Bar Directive', function () {
it('should return the key and value for matching filters', function (done) {
const filter = { missing: { field: '_type' } };
mapMissing(filter).then(function (result) {
- expect(result).to.have.property('key', 'missing');
- expect(result).to.have.property('value', '_type');
+ expect(result).to.have.property('key', '_type');
+ expect(result).to.have.property('value', 'missing');
done();
});
$rootScope.$apply();
diff --git a/src/ui/public/filter_bar/lib/__tests__/map_terms.js b/src/ui/public/filter_bar/lib/__tests__/map_phrase.js
similarity index 79%
rename from src/ui/public/filter_bar/lib/__tests__/map_terms.js
rename to src/ui/public/filter_bar/lib/__tests__/map_phrase.js
index 2ff9f178482154..7fa536d41d7f6e 100644
--- a/src/ui/public/filter_bar/lib/__tests__/map_terms.js
+++ b/src/ui/public/filter_bar/lib/__tests__/map_phrase.js
@@ -1,10 +1,10 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
-import { FilterBarLibMapTermsProvider } from 'ui/filter_bar/lib/map_terms';
+import { FilterBarLibMapPhraseProvider } from '../map_phrase';
describe('Filter Bar Directive', function () {
- describe('mapTerms()', function () {
- let mapTerms;
+ describe('mapPhrase()', function () {
+ let mapPhrase;
let $rootScope;
beforeEach(ngMock.module(
@@ -17,12 +17,12 @@ describe('Filter Bar Directive', function () {
beforeEach(ngMock.inject(function (Private, _$rootScope_) {
$rootScope = _$rootScope_;
- mapTerms = Private(FilterBarLibMapTermsProvider);
+ mapPhrase = Private(FilterBarLibMapPhraseProvider);
}));
it('should return the key and value for matching filters', function (done) {
const filter = { meta: { index: 'logstash-*' }, query: { match: { _type: { query: 'apache', type: 'phrase' } } } };
- mapTerms(filter).then(function (result) {
+ mapPhrase(filter).then(function (result) {
expect(result).to.have.property('key', '_type');
expect(result).to.have.property('value', 'apache');
done();
@@ -32,7 +32,7 @@ describe('Filter Bar Directive', function () {
it('should return undefined for none matching', function (done) {
const filter = { meta: { index: 'logstash-*' }, query: { query_string: { query: 'foo:bar' } } };
- mapTerms(filter).catch(function (result) {
+ mapPhrase(filter).catch(function (result) {
expect(result).to.be(filter);
done();
});
diff --git a/src/ui/public/filter_bar/lib/map_default.js b/src/ui/public/filter_bar/lib/map_default.js
index 1b238c8c964444..292bf92faef14b 100644
--- a/src/ui/public/filter_bar/lib/map_default.js
+++ b/src/ui/public/filter_bar/lib/map_default.js
@@ -11,9 +11,11 @@ export function FilterBarLibMapDefaultProvider(Promise) {
});
if (key) {
+ const type = 'custom';
const value = angular.toJson(filter[key]);
- return Promise.resolve({ key: key, value: value });
+ return Promise.resolve({ type, key, value });
}
+
return Promise.reject(filter);
};
}
diff --git a/src/ui/public/filter_bar/lib/map_exists.js b/src/ui/public/filter_bar/lib/map_exists.js
index 4054d415e0467d..36ada1d127dda4 100644
--- a/src/ui/public/filter_bar/lib/map_exists.js
+++ b/src/ui/public/filter_bar/lib/map_exists.js
@@ -1,11 +1,10 @@
export function FilterBarLibMapExistsProvider(Promise) {
return function (filter) {
- let key;
- let value;
if (filter.exists) {
- key = 'exists';
- value = filter.exists.field;
- return Promise.resolve({ key: key, value: value });
+ const type = 'exists';
+ const key = filter.exists.field;
+ const value = type;
+ return Promise.resolve({ type, key, value });
}
return Promise.reject(filter);
};
diff --git a/src/ui/public/filter_bar/lib/map_filter.js b/src/ui/public/filter_bar/lib/map_filter.js
index 9b81a1ff905386..be8704f1ce089d 100644
--- a/src/ui/public/filter_bar/lib/map_filter.js
+++ b/src/ui/public/filter_bar/lib/map_filter.js
@@ -1,7 +1,8 @@
import _ from 'lodash';
import { FilterBarLibGenerateMappingChainProvider } from './generate_mapping_chain';
import { FilterBarLibMapMatchAllProvider } from './map_match_all';
-import { FilterBarLibMapTermsProvider } from './map_terms';
+import { FilterBarLibMapPhraseProvider } from './map_phrase';
+import { FilterBarLibMapPhrasesProvider } from './map_phrases';
import { FilterBarLibMapRangeProvider } from './map_range';
import { FilterBarLibMapExistsProvider } from './map_exists';
import { FilterBarLibMapMissingProvider } from './map_missing';
@@ -32,8 +33,9 @@ export function FilterBarLibMapFilterProvider(Promise, Private) {
// and add it here. ProTip: These are executed in order listed
const mappers = [
Private(FilterBarLibMapMatchAllProvider),
- Private(FilterBarLibMapTermsProvider),
Private(FilterBarLibMapRangeProvider),
+ Private(FilterBarLibMapPhraseProvider),
+ Private(FilterBarLibMapPhrasesProvider),
Private(FilterBarLibMapExistsProvider),
Private(FilterBarLibMapMissingProvider),
Private(FilterBarLibMapQueryStringProvider),
@@ -63,6 +65,7 @@ export function FilterBarLibMapFilterProvider(Promise, Private) {
// Apply the mapping function
return mapFn(filter).then(function (result) {
filter.meta = filter.meta || {};
+ filter.meta.type = result.type;
filter.meta.key = result.key;
filter.meta.value = result.value;
filter.meta.disabled = !!(filter.meta.disabled);
diff --git a/src/ui/public/filter_bar/lib/map_geo_bounding_box.js b/src/ui/public/filter_bar/lib/map_geo_bounding_box.js
index 7e30db0286fbff..7903ae4f313531 100644
--- a/src/ui/public/filter_bar/lib/map_geo_bounding_box.js
+++ b/src/ui/public/filter_bar/lib/map_geo_bounding_box.js
@@ -2,22 +2,19 @@ import _ from 'lodash';
export function FilterBarLibMapGeoBoundingBoxProvider(Promise, courier) {
return function (filter) {
- let key;
- let value;
- let topLeft;
- let bottomRight;
- let field;
if (filter.geo_bounding_box) {
return courier
.indexPatterns
.get(filter.meta.index).then(function (indexPattern) {
- key = _.keys(filter.geo_bounding_box)
+ const type = 'geo_bounding_box';
+ const key = _.keys(filter.geo_bounding_box)
.filter(key => key !== 'ignore_unmapped')[0];
- field = indexPattern.fields.byName[key];
- topLeft = field.format.convert(filter.geo_bounding_box[field.name].top_left);
- bottomRight = field.format.convert(filter.geo_bounding_box[field.name].bottom_right);
- value = topLeft + ' to ' + bottomRight;
- return { key: key, value: value };
+ const field = indexPattern.fields.byName[key];
+ const geoBoundingBox = filter.geo_bounding_box[key];
+ const topLeft = field.format.convert(geoBoundingBox.top_left);
+ const bottomRight = field.format.convert(geoBoundingBox.bottom_right);
+ const value = topLeft + ' to ' + bottomRight;
+ return { type, key, value };
});
}
return Promise.reject(filter);
diff --git a/src/ui/public/filter_bar/lib/map_match_all.js b/src/ui/public/filter_bar/lib/map_match_all.js
index 546f949cb3087b..2d509816f9b3f4 100644
--- a/src/ui/public/filter_bar/lib/map_match_all.js
+++ b/src/ui/public/filter_bar/lib/map_match_all.js
@@ -1,9 +1,10 @@
export function FilterBarLibMapMatchAllProvider(Promise) {
return function (filter) {
if (filter.match_all) {
+ const type = 'match_all';
const key = filter.meta.field;
const value = filter.meta.formattedValue || 'all';
- return Promise.resolve({ key, value });
+ return Promise.resolve({ type, key, value });
}
return Promise.reject(filter);
};
diff --git a/src/ui/public/filter_bar/lib/map_missing.js b/src/ui/public/filter_bar/lib/map_missing.js
index 6dc98abab3b145..6a20dbae5bf714 100644
--- a/src/ui/public/filter_bar/lib/map_missing.js
+++ b/src/ui/public/filter_bar/lib/map_missing.js
@@ -1,11 +1,10 @@
export function FilterBarLibMapMissingProvider(Promise) {
return function (filter) {
- let key;
- let value;
if (filter.missing) {
- key = 'missing';
- value = filter.missing.field;
- return Promise.resolve({ key: key, value: value });
+ const type = 'missing';
+ const key = filter.missing.field;
+ const value = type;
+ return Promise.resolve({ type, key, value });
}
return Promise.reject(filter);
};
diff --git a/src/ui/public/filter_bar/lib/map_phrase.js b/src/ui/public/filter_bar/lib/map_phrase.js
new file mode 100644
index 00000000000000..c334663dec33a9
--- /dev/null
+++ b/src/ui/public/filter_bar/lib/map_phrase.js
@@ -0,0 +1,26 @@
+import _ from 'lodash';
+
+export function FilterBarLibMapPhraseProvider(Promise, courier) {
+ return function (filter) {
+ const isScriptedPhraseFilter = isScriptedPhrase(filter);
+ if (!_.has(filter, ['query', 'match']) && !isScriptedPhraseFilter) {
+ return Promise.reject(filter);
+ }
+
+ return courier
+ .indexPatterns
+ .get(filter.meta.index).then(function (indexPattern) {
+ const type = 'phrase';
+ const key = isScriptedPhraseFilter ? filter.meta.field : Object.keys(filter.query.match)[0];
+ const field = indexPattern.fields.byName[key];
+ const query = isScriptedPhraseFilter ? filter.script.script.params.value : filter.query.match[key].query;
+ const value = field.format.convert(query);
+ return { type, key, value };
+ });
+ };
+}
+
+function isScriptedPhrase(filter) {
+ const params = _.get(filter, ['script', 'script', 'params']);
+ return params && params.value;
+}
diff --git a/src/ui/public/filter_bar/lib/map_phrases.js b/src/ui/public/filter_bar/lib/map_phrases.js
new file mode 100644
index 00000000000000..f824f50bd24cd7
--- /dev/null
+++ b/src/ui/public/filter_bar/lib/map_phrases.js
@@ -0,0 +1,10 @@
+export function FilterBarLibMapPhrasesProvider(Promise) {
+ return function (filter) {
+ const { type, key, value } = filter.meta;
+ if (type !== 'phrases') {
+ return Promise.reject(filter);
+ } else {
+ return Promise.resolve({ type, key, value });
+ }
+ };
+}
diff --git a/src/ui/public/filter_bar/lib/map_query_string.js b/src/ui/public/filter_bar/lib/map_query_string.js
index cf9ff0eeabefed..b10231140d9f24 100644
--- a/src/ui/public/filter_bar/lib/map_query_string.js
+++ b/src/ui/public/filter_bar/lib/map_query_string.js
@@ -1,11 +1,10 @@
export function FilterBarLibMapQueryStringProvider(Promise) {
return function (filter) {
- let key;
- let value;
if (filter.query && filter.query.query_string) {
- key = 'query';
- value = filter.query.query_string.query;
- return Promise.resolve({ key: key, value: value });
+ const type = 'query_string';
+ const key = 'query';
+ const value = filter.query.query_string.query;
+ return Promise.resolve({ type, key, value });
}
return Promise.reject(filter);
};
diff --git a/src/ui/public/filter_bar/lib/map_range.js b/src/ui/public/filter_bar/lib/map_range.js
index 3e44373db69b00..ac307e2e62c555 100644
--- a/src/ui/public/filter_bar/lib/map_range.js
+++ b/src/ui/public/filter_bar/lib/map_range.js
@@ -1,16 +1,20 @@
-import { has } from 'lodash';
+import { has, get } from 'lodash';
export function FilterBarLibMapRangeProvider(Promise, courier) {
return function (filter) {
- if (!filter.range) return Promise.reject(filter);
+ const isScriptedRangeFilter = isScriptedRange(filter);
+ if (!filter.range && !isScriptedRangeFilter) {
+ return Promise.reject(filter);
+ }
return courier
.indexPatterns
.get(filter.meta.index)
.then(function (indexPattern) {
- const key = Object.keys(filter.range)[0];
+ const type = 'range';
+ const key = isScriptedRangeFilter ? filter.meta.field : Object.keys(filter.range)[0];
const convert = indexPattern.fields.byName[key].format.getConverterFor('text');
- const range = filter.range[key];
+ const range = isScriptedRangeFilter ? filter.script.script.params : filter.range[key];
let left = has(range, 'gte') ? range.gte : range.gt;
if (left == null) left = -Infinity;
@@ -18,11 +22,15 @@ export function FilterBarLibMapRangeProvider(Promise, courier) {
let right = has(range, 'lte') ? range.lte : range.lt;
if (right == null) right = Infinity;
- return {
- key: key,
- value: `${convert(left)} to ${convert(right)}`
- };
+ const value = `${convert(left)} to ${convert(right)}`;
+
+ return { type, key, value };
});
};
}
+
+function isScriptedRange(filter) {
+ const params = get(filter, ['script', 'script', 'params']);
+ return params && Object.keys(params).find(key => ['gte', 'gt', 'lte', 'lt'].includes(key));
+}
diff --git a/src/ui/public/filter_bar/lib/map_script.js b/src/ui/public/filter_bar/lib/map_script.js
index ba5fd490c1b55b..90674aa3f6017c 100644
--- a/src/ui/public/filter_bar/lib/map_script.js
+++ b/src/ui/public/filter_bar/lib/map_script.js
@@ -1,15 +1,14 @@
export function FilterBarLibMapScriptProvider(Promise, courier) {
return function (filter) {
- let key;
- let value;
- let field;
if (filter.script) {
return courier
.indexPatterns
.get(filter.meta.index).then(function (indexPattern) {
- key = filter.meta.field;
- field = indexPattern.fields.byName[key];
+ const type = 'scripted';
+ const key = filter.meta.field;
+ const field = indexPattern.fields.byName[key];
+ let value;
if (filter.meta.formattedValue) {
value = filter.meta.formattedValue;
} else {
@@ -17,7 +16,7 @@ export function FilterBarLibMapScriptProvider(Promise, courier) {
value = field.format.convert(value);
}
- return { key: key, value: value };
+ return { type, key, value };
});
}
return Promise.reject(filter);
diff --git a/src/ui/public/filter_bar/lib/map_terms.js b/src/ui/public/filter_bar/lib/map_terms.js
deleted file mode 100644
index ab837c48bd8d2d..00000000000000
--- a/src/ui/public/filter_bar/lib/map_terms.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import _ from 'lodash';
-
-export function FilterBarLibMapTermsProvider(Promise, courier) {
- return function (filter) {
- let key;
- let value;
- let field;
- if (filter.query && filter.query.match) {
- return courier
- .indexPatterns
- .get(filter.meta.index).then(function (indexPattern) {
- key = _.keys(filter.query.match)[0];
- field = indexPattern.fields.byName[key];
- value = filter.query.match[key].query;
- value = field.format.convert(value);
- return { key: key, value: value };
- });
- }
- return Promise.reject(filter);
- };
-}
diff --git a/src/ui/public/filter_bar/query_filter.js b/src/ui/public/filter_bar/query_filter.js
index 12f67fc445adc7..e552b908bc6829 100644
--- a/src/ui/public/filter_bar/query_filter.js
+++ b/src/ui/public/filter_bar/query_filter.js
@@ -3,7 +3,6 @@ import { onlyDisabled } from 'ui/filter_bar/lib/only_disabled';
import { onlyStateChanged } from 'ui/filter_bar/lib/only_state_changed';
import { uniqFilters } from 'ui/filter_bar/lib/uniq_filters';
import { compareFilters } from 'ui/filter_bar/lib/compare_filters';
-import angular from 'angular';
import { EventsProvider } from 'ui/events';
import { FilterBarLibMapAndFlattenFiltersProvider } from 'ui/filter_bar/lib/map_and_flatten_filters';
@@ -100,26 +99,6 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g
state.filters.splice(index, 1);
};
- /**
- * Updates an existing filter
- * @param {object} filter Contains a reference to a filter and its new model
- * @param {object} filter.source The filter reference
- * @param {string} filter.model The edited filter
- * @returns {object} Promise that resolves to the new filter on a successful merge
- */
- queryFilter.updateFilter = function (filter) {
- const mergedFilter = _.assign({}, filter.source, filter.model);
- mergedFilter.meta.alias = filter.alias;
- //If the filter type is changed we want to discard the old type
- //when merging changes back in
- const filterTypeReplaced = filter.model[filter.type] !== mergedFilter[filter.type];
- if (filterTypeReplaced) {
- delete mergedFilter[filter.type];
- }
-
- return angular.copy(mergedFilter, filter.source);
- };
-
/**
* Removes all filters
*/
@@ -189,13 +168,13 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g
if (!_.isArray(globalState.filters)) globalState.filters = [];
if (!_.isArray(appState.filters)) appState.filters = [];
- const appIndex = _.indexOf(appState.filters, filter);
+ const appIndex = _.findIndex(appState.filters, appFilter => _.isEqual(appFilter, filter));
if (appIndex !== -1 && force !== false) {
appState.filters.splice(appIndex, 1);
globalState.filters.push(filter);
} else {
- const globalIndex = _.indexOf(globalState.filters, filter);
+ const globalIndex = _.findIndex(globalState.filters, globalFilter => _.isEqual(globalFilter, filter));
if (globalIndex === -1 || force === true) return filter;
@@ -383,4 +362,3 @@ export function FilterBarQueryFilterProvider(Private, $rootScope, getAppState, g
}
}
}
-
diff --git a/src/ui/public/filter_editor/filter_editor.html b/src/ui/public/filter_editor/filter_editor.html
new file mode 100644
index 00000000000000..e75e3de9dc777f
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_editor.html
@@ -0,0 +1,132 @@
+
+
diff --git a/src/ui/public/filter_editor/filter_editor.js b/src/ui/public/filter_editor/filter_editor.js
new file mode 100644
index 00000000000000..bd62d793145519
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_editor.js
@@ -0,0 +1,127 @@
+import _ from 'lodash';
+import { uiModules } from 'ui/modules';
+import { FILTER_OPERATOR_TYPES } from './lib/filter_operators';
+import template from './filter_editor.html';
+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
+} from './lib/filter_editor_utils';
+import * as filterBuilder from '../filter_manager/lib';
+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: function ($scope, $element) {
+ this.init = () => {
+ 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.showQueryDslEditor = () => {
+ const { type, isNew } = this.filter.meta;
+ return this.isEditingQueryDsl || (!isNew && !FILTER_OPERATOR_TYPES.includes(type));
+ };
+
+ this.isValid = () => {
+ if (this.showQueryDslEditor()) {
+ 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.showQueryDslEditor()) {
+ 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 = _.get(filter, ['$state', 'store']) === 'globalState';
+ return this.onSave({ filter, newFilter, isPinned });
+ };
+
+ $element.on('keydown', (event) => {
+ if (keyMap[event.keyCode] === 'escape') {
+ $timeout(() => this.onCancel());
+ }
+ });
+ }
+ };
+});
diff --git a/src/ui/public/filter_editor/filter_editor.less b/src/ui/public/filter_editor/filter_editor.less
new file mode 100644
index 00000000000000..d50b1ff527f9d2
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_editor.less
@@ -0,0 +1,30 @@
+@import (reference) "~ui/styles/mixins";
+
+.filterEditor {
+ position: absolute;
+ width: 600px;
+ z-index: 101;
+}
+
+.filterEditor__wideField {
+ min-width: 0;
+}
+
+.uiSelectChoices--autoWidth {
+ width: auto !important;
+ min-width: 100% !important;
+}
+
+.uiSelectMatch--ellipsis {
+ .ellipsis();
+}
+
+.uiSelectMatch--restrictToParent .ui-select-match-item {
+ max-width: 100%;
+}
+
+ .uiSelectMatch--pillWithTooltip {
+ display: block;
+ margin-right: 16px;
+ .ellipsis();
+ }
diff --git a/src/ui/public/filter_editor/filter_field_select.html b/src/ui/public/filter_editor/filter_field_select.html
new file mode 100644
index 00000000000000..dfb6a6198df1b5
--- /dev/null
+++ b/src/ui/public/filter_editor/filter_field_select.html
@@ -0,0 +1,21 @@
+
+
+
+
+ Add
+ Edit
+ filter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Label +
+
+
+
+
+
+
+
+
+
+
+
+ + More actions +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_input_type.js b/src/ui/public/filter_editor/params_editor/filter_params_input_type.js
new file mode 100644
index 00000000000000..5e55b76d7c8d86
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_input_type.js
@@ -0,0 +1,18 @@
+import { uiModules } from 'ui/modules';
+import template from './filter_params_input_type.html';
+import '../../directives/validate_date_math';
+import '../../directives/validate_ip';
+
+const module = uiModules.get('kibana');
+module.directive('filterParamsInputType', function () {
+ return {
+ restrict: 'E',
+ template,
+ scope: {
+ type: '=',
+ placeholder: '@',
+ value: '=',
+ onChange: '&'
+ }
+ };
+});
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js b/src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js
new file mode 100644
index 00000000000000..44012cb1f93b98
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_phrase_controller.js
@@ -0,0 +1,34 @@
+import _ from 'lodash';
+import chrome from 'ui/chrome';
+
+export function filterParamsPhraseController($http, $scope) {
+ 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 (!_.get(field, 'aggregatable') || field.type !== 'string') {
+ return Promise.resolve([]);
+ }
+
+ const params = {
+ query,
+ field: field.name
+ };
+
+ return $http.post(chrome.addBasePath(`/api/kibana/suggestions/values/${field.indexPattern.id}`), params)
+ .then(response => response.data)
+ .catch(() => []);
+ }
+
+ function getFieldQueryHash(field, query) {
+ return `${field.indexPattern.id}/${field.name}/${query}`;
+ }
+}
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html
new file mode 100644
index 00000000000000..bcc8a77ed8c7e7
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_phrase_editor.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+ Accepted date formats
+
+
diff --git a/src/ui/public/filter_editor/params_editor/filter_params_range_editor.js b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.js
new file mode 100644
index 00000000000000..a7cb007f521d0f
--- /dev/null
+++ b/src/ui/public/filter_editor/params_editor/filter_params_range_editor.js
@@ -0,0 +1,20 @@
+import { uiModules } from 'ui/modules';
+import template from './filter_params_range_editor.html';
+import { documentationLinks } from 'ui/documentation_links/documentation_links';
+import './filter_params_input_type';
+import '../../directives/focus_on';
+
+const module = uiModules.get('kibana');
+module.directive('filterParamsRangeEditor', function () {
+ return {
+ restrict: 'E',
+ template,
+ scope: {
+ field: '=',
+ params: '='
+ },
+ link: function (scope) {
+ scope.dateDocLinks = documentationLinks.date;
+ }
+ };
+});
diff --git a/src/ui/public/filter_manager/__tests__/filter_manager.js b/src/ui/public/filter_manager/__tests__/filter_manager.js
index a9aae62a084d3b..7d9b734c9366d4 100644
--- a/src/ui/public/filter_manager/__tests__/filter_manager.js
+++ b/src/ui/public/filter_manager/__tests__/filter_manager.js
@@ -5,7 +5,7 @@ import expect from 'expect.js';
import ngMock from 'ng_mock';
import { FilterManagerProvider } from 'ui/filter_manager';
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
-import { buildInlineScriptForPhraseFilter } from '../lib/phrase';
+import { getPhraseScript } from '../lib/phrase';
let queryFilter;
let filterManager;
let appState;
@@ -117,13 +117,7 @@ describe('Filter Manager', function () {
filterManager.add(scriptedField, 1, '+', 'myIndex');
checkAddFilters(1, [{
meta: { index: 'myIndex', negate: false, field: 'scriptedField' },
- script: {
- script: {
- inline: buildInlineScriptForPhraseFilter(scriptedField),
- lang: scriptedField.lang,
- params: { value: 1 }
- }
- }
+ script: getPhraseScript(scriptedField, 1)
}], 4);
expect(appState.filters).to.have.length(3);
diff --git a/src/ui/public/filter_manager/filter_manager.js b/src/ui/public/filter_manager/filter_manager.js
index 677c5193dbe315..5226956ea5406d 100644
--- a/src/ui/public/filter_manager/filter_manager.js
+++ b/src/ui/public/filter_manager/filter_manager.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
-import { buildInlineScriptForPhraseFilter } from './lib/phrase';
+import { getPhraseScript } from './lib/phrase';
// Adds a filter to a passed state
export function FilterManagerProvider(Private) {
@@ -45,10 +45,7 @@ export function FilterManagerProvider(Private) {
switch (fieldName) {
case '_exists_':
filter = {
- meta: {
- negate: negate,
- index: index
- },
+ meta: { negate, index },
exists: {
field: value
}
@@ -57,19 +54,11 @@ export function FilterManagerProvider(Private) {
default:
if (field.scripted) {
filter = {
- meta: { negate: negate, index: index, field: fieldName },
- script: {
- script: {
- inline: buildInlineScriptForPhraseFilter(field),
- lang: field.lang,
- params: {
- value: value
- }
- }
- }
+ meta: { negate, index, field: fieldName },
+ script: getPhraseScript(field, value)
};
} else {
- filter = { meta: { negate: negate, index: index }, query: { match: {} } };
+ filter = { meta: { negate, index }, query: { match: {} } };
filter.query.match[fieldName] = { query: value, type: 'phrase' };
}
@@ -84,5 +73,3 @@ export function FilterManagerProvider(Private) {
return filterManager;
}
-
-
diff --git a/src/ui/public/filter_manager/lib/exists.js b/src/ui/public/filter_manager/lib/exists.js
new file mode 100644
index 00000000000000..cc495971f74bbb
--- /dev/null
+++ b/src/ui/public/filter_manager/lib/exists.js
@@ -0,0 +1,10 @@
+export function buildExistsFilter(field, indexPattern) {
+ return {
+ meta: {
+ index: indexPattern.id
+ },
+ exists: {
+ field: field.name
+ }
+ };
+}
diff --git a/src/ui/public/filter_manager/lib/index.js b/src/ui/public/filter_manager/lib/index.js
new file mode 100644
index 00000000000000..b3fb9fc80e5c55
--- /dev/null
+++ b/src/ui/public/filter_manager/lib/index.js
@@ -0,0 +1,5 @@
+export { buildExistsFilter } from './exists';
+export { buildPhraseFilter } from './phrase';
+export { buildPhrasesFilter } from './phrases';
+export { buildQueryFilter } from './query';
+export { buildRangeFilter } from './range';
diff --git a/src/ui/public/filter_manager/lib/phrase.js b/src/ui/public/filter_manager/lib/phrase.js
index 78de46be288557..914a51305e4618 100644
--- a/src/ui/public/filter_manager/lib/phrase.js
+++ b/src/ui/public/filter_manager/lib/phrase.js
@@ -1,29 +1,8 @@
-import _ from 'lodash';
-
export function buildPhraseFilter(field, value, indexPattern) {
const filter = { meta: { index: indexPattern.id } };
if (field.scripted) {
- // See https://github.com/elastic/elasticsearch/issues/20941 and https://github.com/elastic/kibana/issues/8677
- // and https://github.com/elastic/elasticsearch/pull/22201
- // for the reason behind this change. Aggs now return boolean buckets with a key of 1 or 0.
- let convertedValue = value;
- if (typeof value !== 'boolean' && field.type === 'boolean') {
- if (value !== 1 && value !== 0) {
- throw new Error('Boolean scripted fields must return true or false');
- }
- convertedValue = value === 1 ? true : false;
- }
-
- const script = buildInlineScriptForPhraseFilter(field);
-
- _.set(filter, 'script.script', {
- inline: script,
- lang: field.lang,
- params: {
- value: convertedValue
- }
- });
+ filter.script = getPhraseScript(field, value);
filter.meta.field = field.name;
} else {
filter.query = { match: {} };
@@ -35,6 +14,33 @@ export function buildPhraseFilter(field, value, indexPattern) {
return filter;
}
+export function getPhraseScript(field, value) {
+ const convertedValue = getConvertedValueForField(field, value);
+ const script = buildInlineScriptForPhraseFilter(field);
+
+ return {
+ script: {
+ inline: script,
+ lang: field.lang,
+ params: {
+ value: convertedValue
+ }
+ }
+ };
+}
+
+// See https://github.com/elastic/elasticsearch/issues/20941 and https://github.com/elastic/kibana/issues/8677
+// and https://github.com/elastic/elasticsearch/pull/22201
+// for the reason behind this change. Aggs now return boolean buckets with a key of 1 or 0.
+function getConvertedValueForField(field, value) {
+ if (typeof value !== 'boolean' && field.type === 'boolean') {
+ if (value !== 1 && value !== 0) {
+ throw new Error('Boolean scripted fields must return true or false');
+ }
+ return value === 1 ? true : false;
+ }
+ return value;
+}
/**
* Takes a scripted field and returns an inline script appropriate for use in a script query.
diff --git a/src/ui/public/filter_manager/lib/phrases.js b/src/ui/public/filter_manager/lib/phrases.js
new file mode 100644
index 00000000000000..a462957bbf083d
--- /dev/null
+++ b/src/ui/public/filter_manager/lib/phrases.js
@@ -0,0 +1,36 @@
+import { getPhraseScript } from './phrase';
+
+export function buildPhrasesFilter(field, params, indexPattern) {
+ const index = indexPattern.id;
+ const type = 'phrases';
+ const key = field.name;
+ const value = params
+ .map(value => field.format.convert(value))
+ .join(', ');
+
+ const filter = {
+ meta: { index, type, key, value, params }
+ };
+
+ let should;
+ if (field.scripted) {
+ should = params.map((value) => ({
+ script: getPhraseScript(field, value)
+ }));
+ } else {
+ should = params.map((value) => ({
+ match_phrase: {
+ [field.name]: value
+ }
+ }));
+ }
+
+ filter.query = {
+ bool: {
+ should,
+ minimum_should_match: 1
+ }
+ };
+
+ return filter;
+}
diff --git a/src/ui/public/filter_manager/lib/range.js b/src/ui/public/filter_manager/lib/range.js
index 53bf0fd735bdc9..faadacd837fbad 100644
--- a/src/ui/public/filter_manager/lib/range.js
+++ b/src/ui/public/filter_manager/lib/range.js
@@ -5,7 +5,9 @@ export function buildRangeFilter(field, params, indexPattern, formattedValue) {
const filter = { meta: { index: indexPattern.id } };
if (formattedValue) filter.meta.formattedValue = formattedValue;
- params = _.clone(params);
+ params = _.mapValues(params, (value) => {
+ return (field.type === 'number') ? parseFloat(value) : value;
+ });
if ('gte' in params && 'gt' in params) throw new Error('gte and gt are mutually exclusive');
if ('lte' in params && 'lt' in params) throw new Error('lte and lt are mutually exclusive');
diff --git a/src/ui/public/stringify/__tests__/_date.js b/src/ui/public/stringify/__tests__/_date.js
index bb09831f6f054d..a2a2d3fe74742b 100644
--- a/src/ui/public/stringify/__tests__/_date.js
+++ b/src/ui/public/stringify/__tests__/_date.js
@@ -45,4 +45,8 @@ describe('Date Format', function () {
expect(chicagoTime).not.to.equal(phoenixTime);
off();
});
+
+ it('should parse date math values', function () {
+ expect(convert('2015-01-01||+1M/d')).to.be('January 1st 2015, 00:00:00.000');
+ });
});
diff --git a/src/ui/public/stringify/types/date.js b/src/ui/public/stringify/types/date.js
index a7f7a1f25cbcad..3717c2779b3d99 100644
--- a/src/ui/public/stringify/types/date.js
+++ b/src/ui/public/stringify/types/date.js
@@ -57,7 +57,13 @@ export function stringifyDate(Private) {
if (val === null || val === undefined) {
return '-';
}
- return moment(val).format(pattern);
+
+ const date = moment(val);
+ if (date.isValid()) {
+ return date.format(pattern);
+ } else {
+ return val;
+ }
});
}
diff --git a/src/ui/public/utils/__tests__/sort_prefix_first.js b/src/ui/public/utils/__tests__/sort_prefix_first.js
index d6c22e24cf5272..9d719670fee83f 100644
--- a/src/ui/public/utils/__tests__/sort_prefix_first.js
+++ b/src/ui/public/utils/__tests__/sort_prefix_first.js
@@ -32,4 +32,11 @@ describe('sortPrefixFirst', function () {
expect(result).to.eql([{ name: 'bar' }, { name: 'baz' }, { name: 'foo' }, { name: 'qux' }, { name: 'quux' }]);
expect(array).to.eql([{ name: 'foo' }, { name: 'bar' }, { name: 'baz' }, { name: 'qux' }, { name: 'quux' }]);
});
+
+ it('should handle numbers', function () {
+ const array = [1, 50, 5];
+ const result = sortPrefixFirst(array, 5);
+ expect(result).to.not.be(array);
+ expect(result).to.eql([50, 5, 1]);
+ });
});
diff --git a/src/ui/public/utils/sort_prefix_first.js b/src/ui/public/utils/sort_prefix_first.js
index 7604c7a0f023cf..285b2ede7c8505 100644
--- a/src/ui/public/utils/sort_prefix_first.js
+++ b/src/ui/public/utils/sort_prefix_first.js
@@ -3,8 +3,8 @@ export function sortPrefixFirst(array, prefix, property) {
return [...array].sort(sortPrefixFirstComparator);
function sortPrefixFirstComparator(a, b) {
- const aValue = property ? a[property] : a;
- const bValue = property ? b[property] : b;
+ const aValue = '' + (property ? a[property] : a);
+ const bValue = '' + (property ? b[property] : b);
const bothStartWith = aValue.startsWith(prefix) && bValue.startsWith(prefix);
const neitherStartWith = !aValue.startsWith(prefix) && !bValue.startsWith(prefix);