Expose Kuery API to Visualizations (#13912)
After some trial and error trying to build a generic API that could control both kuery and legacy filters I decided it wasn't going to work well. So instead I'm taking a different approach. I'm making kuery available to visualizations alongside the existing queryFilter. Visualizations can choose to support one or both. Eventually we can just deprecate and remove queryFilter from the vis API. The standard filter_bar_click_handler still supports both, so basic visualizations that rely on that behavior will just work. The vis API has two new members: queryManager - contains two simple methods, one for getting and the other for setting the current app level query. kuery - all of the functions exported by the kuery module, made available so visualization plugins don't have to import them directly from kibana The addition of these two members should provide visualizations with everything they need to read, modify, and update kuery queries.
This commit is contained in:
parent
f1c2d31feb
commit
239d846049
|
@ -24,9 +24,10 @@ import { notify } from 'ui/notify';
|
|||
import { documentationLinks } from 'ui/documentation_links/documentation_links';
|
||||
import { showCloneModal } from './top_nav/show_clone_modal';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
|
||||
import { QueryManagerProvider } from 'ui/query_manager';
|
||||
import { keyCodes } from 'ui_framework/services';
|
||||
import { DashboardContainerAPI } from './dashboard_container_api';
|
||||
import * as filterActions from 'ui/doc_table/actions/filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
||||
const app = uiModules.get('app/dashboard', [
|
||||
'elasticsearch',
|
||||
|
@ -89,6 +90,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
restrict: 'E',
|
||||
controllerAs: 'dashboardApp',
|
||||
controller: function ($scope, $rootScope, $route, $routeParams, $location, getAppState, $compile, dashboardConfig) {
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const filterBar = Private(FilterBarQueryFilterProvider);
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
const notify = new Notifier({ location: 'Dashboard' });
|
||||
|
@ -101,8 +103,13 @@ app.directive('dashboardApp', function ($injector) {
|
|||
|
||||
const dashboardState = new DashboardState(dash, AppState, dashboardConfig.getHideWriteControls());
|
||||
$scope.appState = dashboardState.getAppState();
|
||||
const queryManager = Private(QueryManagerProvider)(dashboardState.getAppState());
|
||||
$scope.containerApi = new DashboardContainerAPI(dashboardState, queryManager);
|
||||
$scope.containerApi = new DashboardContainerAPI(
|
||||
dashboardState,
|
||||
(field, value, operator, index) => {
|
||||
filterActions.addFilter(field, value, operator, index, dashboardState.getAppState(), filterManager);
|
||||
dashboardState.saveState();
|
||||
}
|
||||
);
|
||||
|
||||
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
|
||||
// normal cross app navigation.
|
||||
|
@ -230,16 +237,6 @@ app.directive('dashboardApp', function ($injector) {
|
|||
|
||||
$scope.$watch('model.query', $scope.updateQueryAndFetch);
|
||||
|
||||
$scope.$watchCollection(() => dashboardState.getAppState().$newFilters, function (filters = []) {
|
||||
// need to convert filters generated from user interaction with viz into kuery AST
|
||||
// These are handled by the filter bar directive when lucene is the query language
|
||||
Promise.all(filters.map(queryManager.addLegacyFilter))
|
||||
.then(() => dashboardState.getAppState().$newFilters = [])
|
||||
.then(updateState)
|
||||
.then(() => dashboardState.applyFilters($scope.model.query, filterBar.getFilters()))
|
||||
.then($scope.refresh());
|
||||
});
|
||||
|
||||
$scope.$listen(timefilter, 'fetch', $scope.refresh);
|
||||
|
||||
function updateViewMode(newMode) {
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { ContainerAPI } from 'ui/embeddable';
|
||||
|
||||
export class DashboardContainerAPI extends ContainerAPI {
|
||||
constructor(dashboardState, queryManager) {
|
||||
constructor(dashboardState, addFilter) {
|
||||
super();
|
||||
this.dashboardState = dashboardState;
|
||||
this.queryManager = queryManager;
|
||||
}
|
||||
|
||||
addFilter(field, value, operator, index) {
|
||||
this.queryManager.add(field, value, operator, index);
|
||||
this.addFilter = addFilter;
|
||||
}
|
||||
|
||||
updatePanel(panelIndex, panelAttributes) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||
import angular from 'angular';
|
||||
import { getSort } from 'ui/doc_table/lib/get_sort';
|
||||
import * as columnActions from 'ui/doc_table/actions/columns';
|
||||
import * as filterActions from 'ui/doc_table/actions/filter';
|
||||
import dateMath from '@elastic/datemath';
|
||||
import 'ui/doc_table';
|
||||
import 'ui/visualize';
|
||||
|
@ -28,7 +29,7 @@ import indexTemplate from 'plugins/kibana/discover/index.html';
|
|||
import { StateProvider } from 'ui/state_management/state';
|
||||
import { documentationLinks } from 'ui/documentation_links/documentation_links';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
|
||||
import { QueryManagerProvider } from 'ui/query_manager';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
|
||||
const app = uiModules.get('apps/discover', [
|
||||
|
@ -118,6 +119,7 @@ function discoverController(
|
|||
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const responseHandler = Private(BasicResponseHandlerProvider).handler;
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const notify = new Notifier({
|
||||
location: 'Discover'
|
||||
});
|
||||
|
@ -176,7 +178,6 @@ function discoverController(
|
|||
};
|
||||
|
||||
const $state = $scope.state = new AppState(getStateDefaults());
|
||||
const queryManager = Private(QueryManagerProvider)($state);
|
||||
|
||||
const getFieldCounts = async () => {
|
||||
// the field counts aren't set until we have the data back,
|
||||
|
@ -332,15 +333,6 @@ function discoverController(
|
|||
$scope.fetch();
|
||||
});
|
||||
|
||||
// Necessary for handling new time filters when the date histogram is clicked
|
||||
$scope.$watchCollection('state.$newFilters', function (filters = []) {
|
||||
// need to convert filters generated from user interaction with viz into kuery AST
|
||||
// These are handled by the filter bar directive when lucene is the query language
|
||||
Promise.all(filters.map(queryManager.addLegacyFilter))
|
||||
.then(() => $scope.state.$newFilters = [])
|
||||
.then($scope.fetch);
|
||||
});
|
||||
|
||||
$scope.$watch('vis.aggs', function () {
|
||||
// no timefield, no vis, nothing to update
|
||||
if (!$scope.opts.timefield) return;
|
||||
|
@ -607,7 +599,7 @@ function discoverController(
|
|||
// TODO: On array fields, negating does not negate the combination, rather all terms
|
||||
$scope.filterQuery = function (field, values, operation) {
|
||||
$scope.indexPattern.popularizeField(field, 1);
|
||||
queryManager.add(field, values, operation, $scope.indexPattern.id);
|
||||
filterActions.addFilter(field, values, operation, $scope.indexPattern.id, $scope.state, filterManager);
|
||||
};
|
||||
|
||||
$scope.addColumn = function addColumn(columnName) {
|
||||
|
|
|
@ -22,7 +22,6 @@ import { documentationLinks } from 'ui/documentation_links/documentation_links';
|
|||
import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url';
|
||||
import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
|
||||
import { QueryManagerProvider } from 'ui/query_manager';
|
||||
|
||||
uiRoutes
|
||||
.when(VisualizeConstants.CREATE_PATH, {
|
||||
|
@ -154,7 +153,6 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
|
|||
|
||||
return appState;
|
||||
}());
|
||||
const queryManager = Private(QueryManagerProvider)($state);
|
||||
|
||||
function init() {
|
||||
// export some objects
|
||||
|
@ -188,14 +186,6 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
|
|||
$appStatus.dirty = status.dirty || !savedVis.id;
|
||||
});
|
||||
|
||||
$scope.$watchCollection('state.$newFilters', function (filters = []) {
|
||||
// need to convert filters generated from user interaction with viz into kuery AST
|
||||
// These are handled by the filter bar directive when lucene is the query language
|
||||
Promise.all(filters.map(queryManager.addLegacyFilter))
|
||||
.then(() => $scope.state.$newFilters = [])
|
||||
.then($scope.fetch);
|
||||
});
|
||||
|
||||
$scope.$watch('state.query', $scope.updateQueryAndFetch);
|
||||
|
||||
$state.replace();
|
||||
|
|
|
@ -8,7 +8,7 @@ import { EmbeddableHandler } from 'ui/embeddable';
|
|||
import chrome from 'ui/chrome';
|
||||
|
||||
export class VisualizeEmbeddableHandler extends EmbeddableHandler {
|
||||
constructor($compile, $rootScope, visualizeLoader, timefilter, Notifier, Promise) {
|
||||
constructor($compile, $rootScope, visualizeLoader, timefilter, Notifier, Promise, Private) {
|
||||
super();
|
||||
this.$compile = $compile;
|
||||
this.visualizeLoader = visualizeLoader;
|
||||
|
@ -16,7 +16,7 @@ export class VisualizeEmbeddableHandler extends EmbeddableHandler {
|
|||
this.name = 'visualization';
|
||||
this.Promise = Promise;
|
||||
this.brushEvent = utilsBrushEventProvider(timefilter);
|
||||
this.filterBarClickHandler = filterBarClickHandlerProvider(Notifier);
|
||||
this.filterBarClickHandler = filterBarClickHandlerProvider(Notifier, Private);
|
||||
}
|
||||
|
||||
getEditPath(panelId) {
|
||||
|
|
|
@ -8,8 +8,9 @@ export function visualizeEmbeddableHandlerProvider(Private) {
|
|||
savedVisualizations,
|
||||
timefilter,
|
||||
Notifier,
|
||||
Promise) => {
|
||||
return new VisualizeEmbeddableHandler($compile, $rootScope, savedVisualizations, timefilter, Notifier, Promise);
|
||||
Promise,
|
||||
Private) => {
|
||||
return new VisualizeEmbeddableHandler($compile, $rootScope, savedVisualizations, timefilter, Notifier, Promise, Private);
|
||||
};
|
||||
return Private(VisualizeEmbeddableHandlerProvider);
|
||||
}
|
||||
|
|
76
src/ui/public/doc_table/__tests__/actions/filter.js
Normal file
76
src/ui/public/doc_table/__tests__/actions/filter.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { addFilter } from '../../actions/filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import NoDigestPromises from 'test_utils/no_digest_promises';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
|
||||
describe('doc table filter actions', function () {
|
||||
NoDigestPromises.activateForSuite();
|
||||
|
||||
let filterManager;
|
||||
let indexPattern;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
function ($provide) {
|
||||
$provide.service('courier', require('fixtures/mock_courier'));
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
filterManager = Private(FilterManagerProvider);
|
||||
sinon.stub(filterManager, 'add');
|
||||
}));
|
||||
|
||||
describe('add', function () {
|
||||
|
||||
it('should defer to the FilterManager when dealing with a lucene query', function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'lucene' }
|
||||
};
|
||||
const args = ['foo', ['bar'], '+', indexPattern, ];
|
||||
addFilter('foo', ['bar'], '+', indexPattern, state, filterManager);
|
||||
expect(filterManager.add.calledOnce).to.be(true);
|
||||
expect(filterManager.add.calledWith(...args)).to.be(true);
|
||||
});
|
||||
|
||||
it('should add an operator style "is" function to kuery queries' , function () {
|
||||
const state = {
|
||||
query: { query: '', language: 'kuery' }
|
||||
};
|
||||
addFilter('foo', 'bar', '+', indexPattern, state, filterManager);
|
||||
expect(state.query.query).to.be('"foo":"bar"');
|
||||
});
|
||||
|
||||
it('should combine the new clause with any existing query clauses using an implicit "and"' , function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'kuery' }
|
||||
};
|
||||
addFilter('foo', 'bar', '+', indexPattern, state, filterManager);
|
||||
expect(state.query.query).to.be('foo "foo":"bar"');
|
||||
});
|
||||
|
||||
it('should support creation of negated clauses' , function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'kuery' }
|
||||
};
|
||||
addFilter('foo', 'bar', '-', indexPattern, state, filterManager);
|
||||
expect(state.query.query).to.be('foo !"foo":"bar"');
|
||||
});
|
||||
|
||||
it('should add an exists query when the provided field name is "_exists_"' , function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'kuery' }
|
||||
};
|
||||
addFilter('_exists_', 'baz', '+', indexPattern, state, filterManager);
|
||||
expect(state.query.query).to.be('foo exists("baz")');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
36
src/ui/public/doc_table/actions/filter.js
Normal file
36
src/ui/public/doc_table/actions/filter.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import _ from 'lodash';
|
||||
import { toKueryExpression, fromKueryExpression, nodeTypes } from 'ui/kuery';
|
||||
|
||||
export function addFilter(field, values = [], operation, index, state, filterManager) {
|
||||
const fieldName = _.isObject(field) ? field.name : field;
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values];
|
||||
}
|
||||
|
||||
if (state.query.language === 'lucene') {
|
||||
filterManager.add(field, values, operation, index);
|
||||
}
|
||||
|
||||
if (state.query.language === 'kuery') {
|
||||
const negate = operation === '-';
|
||||
const isExistsQuery = fieldName === '_exists_';
|
||||
|
||||
const newQueries = values.map((value) => {
|
||||
const newQuery = isExistsQuery
|
||||
? nodeTypes.function.buildNode('exists', value)
|
||||
: nodeTypes.function.buildNode('is', fieldName, value);
|
||||
|
||||
return negate ? nodeTypes.function.buildNode('not', newQuery) : newQuery;
|
||||
});
|
||||
|
||||
const allQueries = _.isEmpty(state.query.query)
|
||||
? newQueries
|
||||
: [fromKueryExpression(state.query.query), ...newQueries];
|
||||
|
||||
state.query = {
|
||||
query: toKueryExpression(nodeTypes.function.buildNode('and', allQueries, 'implicit')),
|
||||
language: 'kuery'
|
||||
};
|
||||
}
|
||||
}
|
|
@ -2,8 +2,11 @@ import _ from 'lodash';
|
|||
import { dedupFilters } from './lib/dedup_filters';
|
||||
import { uniqFilters } from './lib/uniq_filters';
|
||||
import { findByParam } from 'ui/utils/find_by_param';
|
||||
import { AddFiltersToKueryProvider } from './lib/add_filters_to_kuery';
|
||||
|
||||
export function FilterBarClickHandlerProvider(Notifier, Private) {
|
||||
const addFiltersToKuery = Private(AddFiltersToKueryProvider);
|
||||
|
||||
export function FilterBarClickHandlerProvider(Notifier) {
|
||||
return function ($state) {
|
||||
return function (event, simulate) {
|
||||
const notify = new Notifier({
|
||||
|
@ -57,11 +60,20 @@ export function FilterBarClickHandlerProvider(Notifier) {
|
|||
}
|
||||
|
||||
filters = dedupFilters($state.filters, uniqFilters(filters), { negate: true });
|
||||
// We need to add a bunch of filter deduping here.
|
||||
if (!simulate) {
|
||||
$state.$newFilters = filters;
|
||||
}
|
||||
|
||||
if (!simulate) {
|
||||
if ($state.query.language === 'lucene') {
|
||||
$state.$newFilters = filters;
|
||||
}
|
||||
else if ($state.query.language === 'kuery') {
|
||||
addFiltersToKuery($state, filters)
|
||||
.then(() => {
|
||||
if (_.isFunction($state.save)) {
|
||||
$state.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { AddFiltersToKueryProvider } from '../add_filters_to_kuery';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import NoDigestPromises from 'test_utils/no_digest_promises';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('addFiltersToKuery', function () {
|
||||
NoDigestPromises.activateForSuite();
|
||||
|
||||
let addFiltersToKuery;
|
||||
let filterManager;
|
||||
let timefilter;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
function ($provide) {
|
||||
$provide.service('courier', require('fixtures/mock_courier'));
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (Private, _timefilter_) {
|
||||
timefilter = _timefilter_;
|
||||
addFiltersToKuery = Private(AddFiltersToKueryProvider);
|
||||
filterManager = Private(FilterManagerProvider);
|
||||
sinon.stub(filterManager, 'add');
|
||||
}));
|
||||
|
||||
|
||||
const filters = [{
|
||||
meta: {
|
||||
index: 'logstash-*',
|
||||
type: 'phrase',
|
||||
key: 'machine.os',
|
||||
params: {
|
||||
query: 'osx'
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match: {
|
||||
'machine.os': {
|
||||
query: 'osx',
|
||||
type: 'phrase'
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
it('should return a Promise', function () {
|
||||
const state = {
|
||||
query: { query: '', language: 'lucene' }
|
||||
};
|
||||
expect(addFiltersToKuery(state, filters)).to.be.a(Promise);
|
||||
});
|
||||
|
||||
it('should add a query clause equivalent to the given filter', function () {
|
||||
const state = {
|
||||
query: { query: '', language: 'kuery' }
|
||||
};
|
||||
return addFiltersToKuery(state, filters)
|
||||
.then(() => {
|
||||
expect(state.query.query).to.be('"machine.os":"osx"');
|
||||
});
|
||||
});
|
||||
|
||||
it('time field filters should update the global time filter instead of modifying the query', function () {
|
||||
const startTime = moment('1995');
|
||||
const endTime = moment('1996');
|
||||
const state = {
|
||||
query: { query: '', language: 'kuery' }
|
||||
};
|
||||
const timestampFilter = {
|
||||
meta: {
|
||||
index: 'logstash-*',
|
||||
},
|
||||
range: {
|
||||
time: {
|
||||
gt: startTime.valueOf(),
|
||||
lt: endTime.valueOf(),
|
||||
}
|
||||
}
|
||||
};
|
||||
return addFiltersToKuery(state, [timestampFilter])
|
||||
.then(() => {
|
||||
expect(state.query.query).to.be('');
|
||||
expect(startTime.isSame(timefilter.time.from)).to.be(true);
|
||||
expect(endTime.isSame(timefilter.time.to)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
39
src/ui/public/filter_bar/lib/add_filters_to_kuery.js
Normal file
39
src/ui/public/filter_bar/lib/add_filters_to_kuery.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import _ from 'lodash';
|
||||
import { FilterBarLibMapAndFlattenFiltersProvider } from 'ui/filter_bar/lib/map_and_flatten_filters';
|
||||
import { FilterBarLibExtractTimeFilterProvider } from 'ui/filter_bar/lib/extract_time_filter';
|
||||
import { FilterBarLibChangeTimeFilterProvider } from 'ui/filter_bar/lib/change_time_filter';
|
||||
import { FilterBarLibFilterOutTimeBasedFilterProvider } from 'ui/filter_bar/lib/filter_out_time_based_filter';
|
||||
import { toKueryExpression, fromKueryExpression, nodeTypes, filterToKueryAST } from 'ui/kuery';
|
||||
|
||||
export function AddFiltersToKueryProvider(Private) {
|
||||
const mapAndFlattenFilters = Private(FilterBarLibMapAndFlattenFiltersProvider);
|
||||
const extractTimeFilter = Private(FilterBarLibExtractTimeFilterProvider);
|
||||
const changeTimeFilter = Private(FilterBarLibChangeTimeFilterProvider);
|
||||
const filterOutTimeBasedFilter = Private(FilterBarLibFilterOutTimeBasedFilterProvider);
|
||||
|
||||
return async function addFiltersToKuery(state, filters) {
|
||||
return mapAndFlattenFilters(filters)
|
||||
.then((results) => {
|
||||
extractTimeFilter(results)
|
||||
.then((timeFilter) => {
|
||||
if (timeFilter) {
|
||||
changeTimeFilter(timeFilter);
|
||||
}
|
||||
});
|
||||
return results;
|
||||
})
|
||||
.then(filterOutTimeBasedFilter)
|
||||
.then((results) => {
|
||||
const newQueries = results.map(filterToKueryAST);
|
||||
const allQueries = _.isEmpty(state.query.query)
|
||||
? newQueries
|
||||
: [fromKueryExpression(state.query.query), ...newQueries];
|
||||
|
||||
state.query = {
|
||||
query: toKueryExpression(nodeTypes.function.buildNode('and', allQueries, 'implicit')),
|
||||
language: 'kuery'
|
||||
};
|
||||
});
|
||||
|
||||
};
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
import { QueryManagerProvider } from '../query_manager';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import NoDigestPromises from 'test_utils/no_digest_promises';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
import moment from 'moment';
|
||||
|
||||
describe('QueryManager', function () {
|
||||
NoDigestPromises.activateForSuite();
|
||||
|
||||
let queryManager;
|
||||
let filterManager;
|
||||
let indexPattern;
|
||||
let timefilter;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
function ($provide) {
|
||||
$provide.service('courier', require('fixtures/mock_courier'));
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (Private, _timefilter_) {
|
||||
timefilter = _timefilter_;
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
queryManager = Private(QueryManagerProvider);
|
||||
filterManager = Private(FilterManagerProvider);
|
||||
sinon.stub(filterManager, 'add');
|
||||
}));
|
||||
|
||||
describe('add', function () {
|
||||
|
||||
it('should defer to the FilterManager when dealing with a lucene query', function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'lucene' }
|
||||
};
|
||||
const args = ['foo', ['bar'], '+', indexPattern];
|
||||
queryManager = queryManager(state);
|
||||
queryManager.add(...args);
|
||||
expect(filterManager.add.calledOnce).to.be(true);
|
||||
expect(filterManager.add.calledWith(...args)).to.be(true);
|
||||
});
|
||||
|
||||
it('should add an operator style "is" function to kuery queries' , function () {
|
||||
const state = {
|
||||
query: { query: '', language: 'kuery' }
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
queryManager.add('foo', 'bar', '+', indexPattern);
|
||||
expect(state.query.query).to.be('"foo":"bar"');
|
||||
});
|
||||
|
||||
it('should combine the new clause with any existing query clauses using an implicit "and"' , function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'kuery' }
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
queryManager.add('foo', 'bar', '+', indexPattern);
|
||||
expect(state.query.query).to.be('foo "foo":"bar"');
|
||||
});
|
||||
|
||||
it('should support creation of negated clauses' , function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'kuery' }
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
queryManager.add('foo', 'bar', '-', indexPattern);
|
||||
expect(state.query.query).to.be('foo !"foo":"bar"');
|
||||
});
|
||||
|
||||
it('should add an exists query when the provided field name is "_exists_"' , function () {
|
||||
const state = {
|
||||
query: { query: 'foo', language: 'kuery' }
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
queryManager.add('_exists_', 'baz', '+', indexPattern);
|
||||
expect(state.query.query).to.be('foo exists("baz")');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('addLegacyFilter', function () {
|
||||
|
||||
const filter = {
|
||||
meta: {
|
||||
index: 'logstash-*',
|
||||
type: 'phrase',
|
||||
key: 'machine.os',
|
||||
params: {
|
||||
query: 'osx'
|
||||
},
|
||||
},
|
||||
query: {
|
||||
match: {
|
||||
'machine.os': {
|
||||
query: 'osx',
|
||||
type: 'phrase'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
it('should return a Promise', function () {
|
||||
const state = {
|
||||
query: { query: '', language: 'lucene' }
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
expect(queryManager.addLegacyFilter(filter)).to.be.a(Promise);
|
||||
});
|
||||
|
||||
// The filter bar directive will handle new filters when lucene is selected
|
||||
it('should do nothing if the query language is not "kuery"', function () {
|
||||
const state = {
|
||||
query: { query: '', language: 'lucene' }
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
return queryManager.addLegacyFilter(filter)
|
||||
.then(() => {
|
||||
expect(state.query.query).to.be('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a query clause equivalent to the given filter', function () {
|
||||
const state = {
|
||||
query: { query: '', language: 'kuery' }
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
return queryManager.addLegacyFilter(filter)
|
||||
.then(() => {
|
||||
expect(state.query.query).to.be('"machine.os":"osx"');
|
||||
});
|
||||
});
|
||||
|
||||
it('time field filters should update the global time filter instead of modifying the query', function () {
|
||||
const startTime = moment('1995');
|
||||
const endTime = moment('1996');
|
||||
const state = {
|
||||
query: { query: '', language: 'kuery' }
|
||||
};
|
||||
const timestampFilter = {
|
||||
meta: {
|
||||
index: 'logstash-*',
|
||||
},
|
||||
range: {
|
||||
time: {
|
||||
gt: startTime.valueOf(),
|
||||
lt: endTime.valueOf(),
|
||||
}
|
||||
}
|
||||
};
|
||||
queryManager = queryManager(state);
|
||||
return queryManager.addLegacyFilter(timestampFilter)
|
||||
.then(() => {
|
||||
expect(state.query.query).to.be('');
|
||||
expect(startTime.isSame(timefilter.time.from)).to.be(true);
|
||||
expect(endTime.isSame(timefilter.time.to)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -1 +1 @@
|
|||
export { QueryManagerProvider } from './query_manager';
|
||||
export { queryManagerFactory } from './query_manager';
|
||||
|
|
|
@ -1,79 +1,24 @@
|
|||
import _ from 'lodash';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { FilterBarLibMapAndFlattenFiltersProvider } from 'ui/filter_bar/lib/map_and_flatten_filters';
|
||||
import { FilterBarLibExtractTimeFilterProvider } from 'ui/filter_bar/lib/extract_time_filter';
|
||||
import { FilterBarLibChangeTimeFilterProvider } from 'ui/filter_bar/lib/change_time_filter';
|
||||
import { toKueryExpression, fromKueryExpression, nodeTypes, filterToKueryAST } from 'ui/kuery';
|
||||
|
||||
export function QueryManagerProvider(Private) {
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const mapAndFlattenFilters = Private(FilterBarLibMapAndFlattenFiltersProvider);
|
||||
const extractTimeFilter = Private(FilterBarLibExtractTimeFilterProvider);
|
||||
const changeTimeFilter = Private(FilterBarLibChangeTimeFilterProvider);
|
||||
|
||||
return function (state) {
|
||||
|
||||
function add(field, values = [], operation, index) {
|
||||
const fieldName = _.isObject(field) ? field.name : field;
|
||||
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values];
|
||||
}
|
||||
|
||||
if (state.query.language === 'lucene') {
|
||||
filterManager.add(field, values, operation, index);
|
||||
}
|
||||
|
||||
if (state.query.language === 'kuery') {
|
||||
const negate = operation === '-';
|
||||
const isExistsQuery = fieldName === '_exists_';
|
||||
|
||||
const newQueries = values.map((value) => {
|
||||
const newQuery = isExistsQuery
|
||||
? nodeTypes.function.buildNode('exists', value)
|
||||
: nodeTypes.function.buildNode('is', fieldName, value);
|
||||
|
||||
return negate ? nodeTypes.function.buildNode('not', newQuery) : newQuery;
|
||||
});
|
||||
|
||||
const allQueries = _.isEmpty(state.query.query)
|
||||
? newQueries
|
||||
: [fromKueryExpression(state.query.query), ...newQueries];
|
||||
|
||||
state.query = {
|
||||
query: toKueryExpression(nodeTypes.function.buildNode('and', allQueries, 'implicit')),
|
||||
language: 'kuery'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function addLegacyFilter(filter) {
|
||||
// The filter_bar directive currently handles filter creation when lucene is the selected language,
|
||||
// so we only handle updating the kuery AST here.
|
||||
if (state.query.language === 'kuery') {
|
||||
const timeFilter = await extractTimeFilter([filter]);
|
||||
if (timeFilter) {
|
||||
changeTimeFilter(timeFilter);
|
||||
}
|
||||
else {
|
||||
const [ mappedFilter ] = await mapAndFlattenFilters([filter]);
|
||||
const newQuery = filterToKueryAST(mappedFilter);
|
||||
const allQueries = _.isEmpty(state.query.query)
|
||||
? [newQuery]
|
||||
: [fromKueryExpression(state.query.query), newQuery];
|
||||
|
||||
state.query = {
|
||||
query: toKueryExpression(nodeTypes.function.buildNode('and', allQueries, 'implicit')),
|
||||
language: 'kuery'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
export function queryManagerFactory(state) {
|
||||
|
||||
function getQuery() {
|
||||
return {
|
||||
add,
|
||||
addLegacyFilter,
|
||||
...state.query
|
||||
};
|
||||
}
|
||||
|
||||
function updateQuery(newQuery) {
|
||||
state.query = newQuery;
|
||||
|
||||
if (_.isFunction(state.save)) {
|
||||
state.save();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getQuery,
|
||||
updateQuery,
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ import { UtilsBrushEventProvider } from 'ui/utils/brush_event';
|
|||
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
|
||||
import { FilterBarClickHandlerProvider } from 'ui/filter_bar/filter_bar_click_handler';
|
||||
import { updateVisualizationConfig } from './vis_update';
|
||||
import { queryManagerFactory } from '../query_manager';
|
||||
import * as kueryAPI from 'ui/kuery';
|
||||
|
||||
export function VisProvider(Private, indexPatterns, timefilter, getAppState) {
|
||||
const visTypes = Private(VisTypesRegistryProvider);
|
||||
|
@ -53,6 +55,8 @@ export function VisProvider(Private, indexPatterns, timefilter, getAppState) {
|
|||
indexPatterns: indexPatterns,
|
||||
timeFilter: timefilter,
|
||||
queryFilter: queryFilter,
|
||||
queryManager: queryManagerFactory(getAppState()),
|
||||
kuery: kueryAPI,
|
||||
events: {
|
||||
filter: (event) => {
|
||||
const appState = getAppState();
|
||||
|
|
Loading…
Reference in a new issue