mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Implement new Kibana query language (#12624)
Initial version of an experimental new query language for Kibana.
This commit is contained in:
parent
1b024200cd
commit
d379e9a35b
102 changed files with 3701 additions and 406 deletions
|
@ -22,6 +22,8 @@ compatible with other configuration settings. Deleting a custom setting removes
|
|||
.Kibana Settings Reference
|
||||
[horizontal]
|
||||
`query:queryString:options`:: Options for the Lucene query string parser.
|
||||
`search:queryLanguage`:: Default is `lucene`. Query language used by the query bar. Choose between the lucene query syntax and kuery, an experimental new language built specifically for Kibana.
|
||||
`search:queryLanguage:switcher:enable`:: Show or hide the query language switcher in the query bar.
|
||||
`sort:options`:: Options for the Elasticsearch {ref}/search-request-sort.html[sort] parameter.
|
||||
`dateFormat`:: The format to use for displaying pretty-formatted dates.
|
||||
`dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser.
|
||||
|
|
|
@ -88,9 +88,12 @@ describe('context app', function () {
|
|||
const setQuerySpy = searchSourceStub.set.withArgs('query');
|
||||
expect(setQuerySpy.calledOnce).to.be(true);
|
||||
expect(setQuerySpy.firstCall.args[1]).to.eql({
|
||||
terms: {
|
||||
_uid: ['UID'],
|
||||
query: {
|
||||
terms: {
|
||||
_uid: ['UID'],
|
||||
}
|
||||
},
|
||||
language: 'lucene'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,9 +15,12 @@ function fetchAnchorProvider(courier, Private) {
|
|||
.set('version', true)
|
||||
.set('size', 1)
|
||||
.set('query', {
|
||||
terms: {
|
||||
_uid: [uid],
|
||||
query: {
|
||||
terms: {
|
||||
_uid: [uid],
|
||||
}
|
||||
},
|
||||
language: 'lucene'
|
||||
})
|
||||
.set('sort', sort);
|
||||
|
||||
|
|
|
@ -50,7 +50,10 @@ function fetchContextProvider(courier, Private) {
|
|||
.set('size', size)
|
||||
.set('filter', filters)
|
||||
.set('query', {
|
||||
match_all: {},
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
language: 'lucene'
|
||||
})
|
||||
.set('searchAfter', anchorDocument.sort)
|
||||
.set('sort', sort);
|
||||
|
|
|
@ -31,54 +31,14 @@
|
|||
</div>
|
||||
|
||||
<!-- Search. -->
|
||||
<form
|
||||
data-transclude-slot="bottomRow"
|
||||
class="fullWidth"
|
||||
ng-show="chrome.getVisible()"
|
||||
role="form"
|
||||
name="queryInput"
|
||||
ng-submit="filterResults()"
|
||||
>
|
||||
<div class="typeahead" kbn-typeahead="dashboard" on-select="filterResults()">
|
||||
<div class="kuiLocalSearch">
|
||||
<div class="kuiLocalSearchAssistedInput">
|
||||
<input
|
||||
parse-query
|
||||
input-focus
|
||||
kbn-typeahead-input
|
||||
ng-model="model.query"
|
||||
placeholder="Search... (e.g. status:200 AND extension:PHP)"
|
||||
aria-label="Enter query"
|
||||
data-test-subj="dashboardQuery"
|
||||
type="text"
|
||||
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
|
||||
ng-class="{'kuiLocalSearchInput-isInvalid': queryInput.$invalid}"
|
||||
>
|
||||
<div class="kuiLocalSearchAssistedInput__assistance">
|
||||
<p class="kuiText">
|
||||
<a
|
||||
class="kuiLink"
|
||||
ng-href="{{queryDocLinks.luceneQuerySyntax}}"
|
||||
target="_blank"
|
||||
>
|
||||
Uses lucene query syntax
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Submit query"
|
||||
class="kuiLocalSearchButton"
|
||||
data-test-subj="dashboardQueryFilterButton"
|
||||
ng-disabled="queryInput.$invalid"
|
||||
>
|
||||
<span class="kuiIcon fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
|
||||
<query-bar
|
||||
query="model.query"
|
||||
app-name="'dashboard'"
|
||||
on-submit="updateQuery($query)"
|
||||
>
|
||||
</query-bar>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
|
@ -87,6 +47,7 @@
|
|||
ng-show="showFilterBar()"
|
||||
state="state"
|
||||
index-patterns="indexPatterns"
|
||||
ng-if="model.query.language === 'lucene'"
|
||||
></filter-bar>
|
||||
|
||||
<div
|
||||
|
@ -126,6 +87,7 @@
|
|||
toggle-expand="toggleExpandPanel"
|
||||
register-panel-index-pattern="registerPanelIndexPattern"
|
||||
data-shared-items-count="{{panels.length}}"
|
||||
on-filter="filter"
|
||||
></dashboard-grid>
|
||||
|
||||
<dashboard-panel
|
||||
|
|
|
@ -6,6 +6,7 @@ import chrome from 'ui/chrome';
|
|||
|
||||
import 'plugins/kibana/dashboard/grid';
|
||||
import 'plugins/kibana/dashboard/panel/panel';
|
||||
import 'ui/query_bar';
|
||||
|
||||
import { SavedObjectNotFound } from 'ui/errors';
|
||||
import { getDashboardTitle, getUnsavedChangesWarningMessage } from './dashboard_strings';
|
||||
|
@ -25,6 +26,8 @@ import { notify } from 'ui/notify';
|
|||
import './panel/get_object_loaders_for_dashboard';
|
||||
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 { ESC_KEY_CODE } from 'ui_framework/services';
|
||||
|
||||
const app = uiModules.get('app/dashboard', [
|
||||
|
@ -81,6 +84,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
const quickRanges = $injector.get('quickRanges');
|
||||
const kbnUrl = $injector.get('kbnUrl');
|
||||
const confirmModal = $injector.get('confirmModal');
|
||||
const config = $injector.get('config');
|
||||
const Private = $injector.get('Private');
|
||||
const brushEvent = Private(UtilsBrushEventProvider);
|
||||
const filterBarClickHandler = Private(FilterBarClickHandlerProvider);
|
||||
|
@ -100,6 +104,7 @@ app.directive('dashboardApp', function ($injector) {
|
|||
}
|
||||
|
||||
const dashboardState = new DashboardState(dash, AppState, dashboardConfig);
|
||||
const queryManager = Private(QueryManagerProvider)(dashboardState.getAppState());
|
||||
|
||||
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
|
||||
// normal cross app navigation.
|
||||
|
@ -130,7 +135,10 @@ app.directive('dashboardApp', function ($injector) {
|
|||
updateState();
|
||||
});
|
||||
|
||||
dashboardState.applyFilters(dashboardState.getQuery(), filterBar.getFilters());
|
||||
dashboardState.applyFilters(
|
||||
dashboardState.getQuery() || { query: '', language: config.get('search:queryLanguage') },
|
||||
filterBar.getFilters()
|
||||
);
|
||||
let pendingVisCount = _.size(dashboardState.getPanels());
|
||||
|
||||
timefilter.enabled = true;
|
||||
|
@ -179,9 +187,14 @@ app.directive('dashboardApp', function ($injector) {
|
|||
}
|
||||
};
|
||||
|
||||
$scope.filterResults = function () {
|
||||
dashboardState.applyFilters($scope.model.query, filterBar.getFilters());
|
||||
$scope.refresh();
|
||||
$scope.updateQuery = function (query) {
|
||||
// reset state if language changes
|
||||
if ($scope.model.query.language && $scope.model.query.language !== query.language) {
|
||||
filterBar.removeAll();
|
||||
dashboardState.getAppState().$newFilters = [];
|
||||
}
|
||||
|
||||
$scope.model.query = query;
|
||||
};
|
||||
|
||||
// called by the saved-object-finder when a user clicks a vis
|
||||
|
@ -229,6 +242,28 @@ app.directive('dashboardApp', function ($injector) {
|
|||
$scope.indexPatterns = dashboardState.getPanelIndexPatterns();
|
||||
};
|
||||
|
||||
$scope.filter = function (field, value, operator, index) {
|
||||
queryManager.add(field, value, operator, index);
|
||||
updateState();
|
||||
};
|
||||
|
||||
|
||||
$scope.$watch('model.query', (newQuery) => {
|
||||
$scope.model.query = migrateLegacyQuery(newQuery);
|
||||
dashboardState.applyFilters($scope.model.query, filterBar.getFilters());
|
||||
$scope.refresh();
|
||||
});
|
||||
|
||||
$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) {
|
||||
|
|
|
@ -2,16 +2,19 @@
|
|||
import _ from 'lodash';
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
|
||||
import 'ui/state_management/app_state';
|
||||
import { luceneStringToDsl } from '../../../../ui/public/courier/data_source/build_query/lucene_string_to_dsl';
|
||||
|
||||
export function dashboardContextProvider(Private, getAppState) {
|
||||
return () => {
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const bool = { must: [], must_not: [] };
|
||||
const filterBarFilters = queryFilter.getFilters();
|
||||
const queryBarFilter = getAppState().query;
|
||||
const queryBarQuery = getAppState().query;
|
||||
|
||||
// Add the query bar filter, its handled differently.
|
||||
bool.must.push(queryBarFilter);
|
||||
if (queryBarQuery.language === 'lucene') {
|
||||
// Add the query bar filter, its handled differently.
|
||||
bool.must.push(luceneStringToDsl(queryBarQuery.query));
|
||||
}
|
||||
|
||||
// Add each of the filter bar filters
|
||||
_.each(filterBarFilters, function (filter) {
|
||||
|
|
|
@ -219,7 +219,19 @@ export class DashboardState {
|
|||
* new dashboard, if the query differs from the default.
|
||||
*/
|
||||
getQueryChanged() {
|
||||
return !_.isEqual(this.appState.query, this.getLastSavedQuery());
|
||||
const currentQuery = this.appState.query;
|
||||
const lastSavedQuery = this.getLastSavedQuery();
|
||||
|
||||
const isLegacyStringQuery = (
|
||||
_.isString(lastSavedQuery)
|
||||
&& _.isPlainObject(currentQuery)
|
||||
&& _.has(currentQuery, 'query')
|
||||
);
|
||||
if (isLegacyStringQuery) {
|
||||
return lastSavedQuery !== currentQuery.query;
|
||||
}
|
||||
|
||||
return !_.isEqual(currentQuery, lastSavedQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -404,14 +416,8 @@ export class DashboardState {
|
|||
*/
|
||||
applyFilters(query, filters) {
|
||||
this.appState.query = query;
|
||||
if (this.appState.query) {
|
||||
this.savedDashboard.searchSource.set('filter', _.union(filters, [{
|
||||
query: this.appState.query
|
||||
}]));
|
||||
} else {
|
||||
this.savedDashboard.searchSource.set('filter', filters);
|
||||
}
|
||||
|
||||
this.savedDashboard.searchSource.set('query', query);
|
||||
this.savedDashboard.searchSource.set('filter', filters);
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
|
@ -424,6 +430,8 @@ export class DashboardState {
|
|||
this.stateMonitor.ignoreProps('viewMode');
|
||||
// Filters need to be compared manually because they sometimes have a $$hashkey stored on the object.
|
||||
this.stateMonitor.ignoreProps('filters');
|
||||
// Query needs to be compared manually because saved legacy queries get migrated in app state automatically
|
||||
this.stateMonitor.ignoreProps('query');
|
||||
|
||||
this.stateMonitor.onChange(status => {
|
||||
this.isDirty = status.dirty;
|
||||
|
|
|
@ -33,10 +33,13 @@ export class FilterUtils {
|
|||
* @returns {QueryFilter}
|
||||
*/
|
||||
static getQueryFilterForDashboard(dashboard) {
|
||||
const defaultQueryFilter = { query_string: { query: '*' } };
|
||||
if (dashboard.searchSource.getOwn('query')) {
|
||||
return dashboard.searchSource.getOwn('query');
|
||||
}
|
||||
|
||||
const dashboardFilters = this.getDashboardFilters(dashboard);
|
||||
const dashboardQueryFilter = _.find(dashboardFilters, this.isQueryFilter);
|
||||
return dashboardQueryFilter ? dashboardQueryFilter.query : defaultQueryFilter;
|
||||
return dashboardQueryFilter ? dashboardQueryFilter.query : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -60,6 +60,11 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
* @type {function}
|
||||
*/
|
||||
toggleExpand: '=',
|
||||
/**
|
||||
* Called when a filter action has been triggered by a panel
|
||||
* @type {function}
|
||||
*/
|
||||
onFilter: '=',
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
const notify = new Notifier();
|
||||
|
@ -223,7 +228,8 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
|
|||
app-state="appState"
|
||||
register-panel-index-pattern="registerPanelIndexPattern"
|
||||
toggle-expand="toggleExpand(${panel.panelIndex})"
|
||||
create-child-ui-state="createChildUiState">
|
||||
create-child-ui-state="createChildUiState"
|
||||
on-filter="onFilter">
|
||||
</li>`;
|
||||
const panelElement = $compile(panelHtml)($scope);
|
||||
panelElementMapping[panel.panelIndex] = panelElement;
|
||||
|
|
|
@ -5,7 +5,6 @@ import * as columnActions from 'ui/doc_table/actions/columns';
|
|||
import 'plugins/kibana/dashboard/panel/get_object_loaders_for_dashboard';
|
||||
import 'plugins/kibana/visualize/saved_visualizations';
|
||||
import 'plugins/kibana/discover/saved_searches';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import panelTemplate from 'plugins/kibana/dashboard/panel/panel.html';
|
||||
import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry';
|
||||
|
@ -16,7 +15,6 @@ import { DashboardViewMode } from '../dashboard_view_mode';
|
|||
uiModules
|
||||
.get('app/dashboard')
|
||||
.directive('dashboardPanel', function (savedVisualizations, savedSearches, Notifier, Private, $injector, getObjectLoadersForDashboard) {
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
|
||||
const services = savedObjectManagementRegistry.all().map(function (serviceObj) {
|
||||
const service = $injector.get(serviceObj.service);
|
||||
|
@ -86,6 +84,11 @@ uiModules
|
|||
* @type {function}
|
||||
*/
|
||||
saveState: '=',
|
||||
/**
|
||||
* Called when a filter action has been triggered
|
||||
* @type {function}
|
||||
*/
|
||||
onFilter: '=',
|
||||
appState: '=',
|
||||
},
|
||||
link: function ($scope, element) {
|
||||
|
@ -148,7 +151,7 @@ uiModules
|
|||
|
||||
$scope.filter = function (field, value, operator) {
|
||||
const index = $scope.savedObj.searchSource.get('index').id;
|
||||
filterManager.add(field, value, operator, index);
|
||||
$scope.onFilter(field, value, operator, index);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ import 'ui/index_patterns';
|
|||
import 'ui/state_management/app_state';
|
||||
import 'ui/timefilter';
|
||||
import 'ui/share';
|
||||
import 'ui/query_bar';
|
||||
import { VisProvider } from 'ui/vis';
|
||||
import { BasicResponseHandlerProvider } from 'ui/vis/response_handlers/basic';
|
||||
import { DocTitleProvider } from 'ui/doc_title';
|
||||
import PluginsKibanaDiscoverHitSortFnProvider from 'plugins/kibana/discover/_hit_sort_fn';
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { AggTypesBucketsIntervalOptionsProvider } from 'ui/agg_types/buckets/_interval_options';
|
||||
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
@ -27,6 +27,8 @@ import { uiModules } from 'ui/modules';
|
|||
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 { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
|
||||
const app = uiModules.get('apps/discover', [
|
||||
|
@ -115,7 +117,6 @@ function discoverController(
|
|||
const docTitle = Private(DocTitleProvider);
|
||||
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const responseHandler = Private(BasicResponseHandlerProvider).handler;
|
||||
const notify = new Notifier({
|
||||
location: 'Discover'
|
||||
|
@ -176,6 +177,7 @@ 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,
|
||||
|
@ -251,7 +253,7 @@ function discoverController(
|
|||
|
||||
function getStateDefaults() {
|
||||
return {
|
||||
query: $scope.searchSource.get('query') || '',
|
||||
query: $scope.searchSource.get('query') || { query: '', language: config.get('search:queryLanguage') },
|
||||
sort: getSort.array(savedSearch.sort, $scope.indexPattern),
|
||||
columns: savedSearch.columns.length > 0 ? savedSearch.columns : config.get('defaultColumns').slice(),
|
||||
index: $scope.indexPattern.id,
|
||||
|
@ -331,6 +333,15 @@ 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;
|
||||
|
@ -342,6 +353,12 @@ function discoverController(
|
|||
}
|
||||
});
|
||||
|
||||
$scope.$watch('state.query', (newQuery) => {
|
||||
$state.query = migrateLegacyQuery(newQuery);
|
||||
|
||||
$scope.fetch();
|
||||
});
|
||||
|
||||
$scope.$watchMulti([
|
||||
'rows',
|
||||
'fetchStatus'
|
||||
|
@ -444,6 +461,15 @@ function discoverController(
|
|||
.catch(notify.error);
|
||||
};
|
||||
|
||||
$scope.updateQuery = function (query) {
|
||||
// reset state if language changes
|
||||
if ($state.query.language && $state.query.language !== query.language) {
|
||||
$state.filters = [];
|
||||
}
|
||||
|
||||
$state.query = query;
|
||||
};
|
||||
|
||||
$scope.searchSource.onBeginSegmentedFetch(function (segmented) {
|
||||
function flushResponseData() {
|
||||
$scope.hits = 0;
|
||||
|
@ -584,7 +610,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);
|
||||
filterManager.add(field, values, operation, $state.index);
|
||||
queryManager.add(field, values, operation, $scope.indexPattern.id);
|
||||
};
|
||||
|
||||
$scope.addColumn = function addColumn(columnName) {
|
||||
|
|
|
@ -16,53 +16,14 @@
|
|||
</div>
|
||||
|
||||
<!-- Search. -->
|
||||
<form
|
||||
data-transclude-slot="bottomRow"
|
||||
class="fullWidth"
|
||||
role="form"
|
||||
name="discoverSearch"
|
||||
ng-submit="fetch()"
|
||||
>
|
||||
<div class="typeahead" kbn-typeahead="discover" on-select="fetch()" role="search">
|
||||
<div class="kuiLocalSearch">
|
||||
<div class="kuiLocalSearchAssistedInput">
|
||||
<input
|
||||
parse-query
|
||||
input-focus
|
||||
kbn-typeahead-input
|
||||
ng-model="state.query"
|
||||
placeholder="Search... (e.g. status:200 AND extension:PHP)"
|
||||
aria-label="Search input"
|
||||
aria-describedby="discover-lucene-syntax-hint"
|
||||
type="text"
|
||||
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
|
||||
ng-class="{'kuiLocalSearchInput-isInvalid': discoverSearch.$invalid}"
|
||||
>
|
||||
<div class="kuiLocalSearchAssistedInput__assistance">
|
||||
<p class="kuiText">
|
||||
<a
|
||||
id="discover-lucene-syntax-hint"
|
||||
class="kuiLink"
|
||||
ng-href="{{queryDocLinks.luceneQuerySyntax}}"
|
||||
target="_blank"
|
||||
>
|
||||
Uses lucene query syntax
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Search"
|
||||
class="kuiLocalSearchButton"
|
||||
ng-disabled="discoverSearch.$invalid"
|
||||
>
|
||||
<span class="kuiIcon fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
||||
<div data-transclude-slot="bottomRow" class="fullWidth">
|
||||
<query-bar
|
||||
query="state.query"
|
||||
app-name="'discover'"
|
||||
on-submit="updateQuery($query)"
|
||||
>
|
||||
</query-bar>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
|
@ -71,6 +32,7 @@
|
|||
<filter-bar
|
||||
state="state"
|
||||
index-patterns="[indexPattern]"
|
||||
ng-if="state.query.language === 'lucene'"
|
||||
></filter-bar>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
|
|
@ -34,12 +34,13 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Refine your query</h3>
|
||||
<p>
|
||||
The search bar at the top uses Elasticsearch's support for Lucene <a class="kuiLink" ng-href="{{queryDocLinks.luceneQuerySyntax}}" target="_blank">Query String syntax</a>. Let's say we're searching web server logs that have been parsed into a few fields.
|
||||
</p>
|
||||
<div ng-if="state.query.language === 'lucene'">
|
||||
<h3>Refine your query</h3>
|
||||
<p>
|
||||
The search bar at the top uses Elasticsearch's support for Lucene <a class="kuiLink" ng-href="{{queryDocLinks.luceneQuerySyntax}}" target="_blank">Query String syntax</a>. Let's say we're searching web server logs that have been parsed into a few fields.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<p>
|
||||
<h4>Examples:</h4>
|
||||
Find requests that contain the number 200, in any field:
|
||||
<pre>200</pre>
|
||||
|
@ -55,7 +56,7 @@
|
|||
|
||||
Or HTML
|
||||
<pre>status:[400 TO 499] AND (extension:php OR extension:html)</pre>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -32,56 +32,21 @@
|
|||
</div>
|
||||
|
||||
<!-- Allow searching if there is no linked Saved Search. -->
|
||||
<form
|
||||
ng-if="vis.type.requiresSearch && vis.type.options.showQueryBar && !state.linked"
|
||||
name="queryInput"
|
||||
ng-submit="fetch()"
|
||||
class="fullWidth"
|
||||
>
|
||||
<div class="typeahead" kbn-typeahead="visualize" on-select="fetch()">
|
||||
<div class="kuiLocalSearch">
|
||||
<div class="kuiLocalSearchAssistedInput">
|
||||
<input
|
||||
ng-model="state.query"
|
||||
parse-query
|
||||
input-focus
|
||||
kbn-typeahead-input
|
||||
placeholder="Search... (e.g. status:200 AND extension:PHP)"
|
||||
type="text"
|
||||
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
|
||||
ng-class="{'kuiLocalSearchInput-isInvalid': queryInput.$invalid}"
|
||||
>
|
||||
<div class="kuiLocalSearchAssistedInput__assistance">
|
||||
<p class="kuiText">
|
||||
<a
|
||||
class="kuiLink"
|
||||
ng-href="{{queryDocLinks.luceneQuerySyntax}}"
|
||||
target="_blank"
|
||||
>
|
||||
Uses lucene query syntax
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Search"
|
||||
class="kuiLocalSearchButton"
|
||||
ng-disabled="queryInput.$invalid"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-search"></span>
|
||||
</button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-if="vis.type.requiresSearch && !state.linked && vis.type.options.showQueryBar" class="fullWidth">
|
||||
<query-bar
|
||||
query="state.query"
|
||||
app-name="'visualize'"
|
||||
on-submit="updateQuery($query)"
|
||||
>
|
||||
</query-bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
<!-- Filters. -->
|
||||
<filter-bar
|
||||
ng-if="vis.type.options.showFilterBar && !state.linked"
|
||||
ng-if="vis.type.options.showFilterBar && state.query.language === 'lucene' && !state.linked"
|
||||
state="state"
|
||||
index-patterns="[indexPattern]"
|
||||
></filter-bar>
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'plugins/kibana/visualize/editor/agg_filter';
|
|||
import 'ui/visualize';
|
||||
import 'ui/collapsible_sidebar';
|
||||
import 'ui/share';
|
||||
import 'ui/query_bar';
|
||||
import chrome from 'ui/chrome';
|
||||
import angular from 'angular';
|
||||
import { Notifier } from 'ui/notify/notifier';
|
||||
|
@ -20,6 +21,8 @@ import { VisualizeConstants } from '../visualize_constants';
|
|||
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, {
|
||||
|
@ -69,7 +72,7 @@ uiModules
|
|||
};
|
||||
});
|
||||
|
||||
function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courier, Private, Promise, kbnBaseUrl) {
|
||||
function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courier, Private, Promise, config, kbnBaseUrl) {
|
||||
const docTitle = Private(DocTitleProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
|
||||
|
@ -127,7 +130,7 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
|
|||
const stateDefaults = {
|
||||
uiState: savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : {},
|
||||
linked: !!savedVis.savedSearchId,
|
||||
query: searchSource.getOwn('query') || { query_string: { query: '*' } },
|
||||
query: searchSource.getOwn('query') || { query: '', language: config.get('search:queryLanguage') },
|
||||
filters: searchSource.getOwn('filter') || [],
|
||||
vis: savedVisState
|
||||
};
|
||||
|
@ -151,6 +154,7 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
|
|||
|
||||
return appState;
|
||||
}());
|
||||
const queryManager = Private(QueryManagerProvider)($state);
|
||||
|
||||
function init() {
|
||||
// export some objects
|
||||
|
@ -184,6 +188,20 @@ 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', (newQuery) => {
|
||||
$state.query = migrateLegacyQuery(newQuery);
|
||||
|
||||
$scope.fetch();
|
||||
});
|
||||
|
||||
$state.replace();
|
||||
|
||||
$scope.getVisualizationTitle = function getVisualizationTitle() {
|
||||
|
@ -218,6 +236,16 @@ function VisEditor($scope, $route, timefilter, AppState, $window, kbnUrl, courie
|
|||
});
|
||||
}
|
||||
|
||||
$scope.updateQuery = function (query) {
|
||||
// reset state if language changes
|
||||
if ($state.query.language && $state.query.language !== query.language) {
|
||||
$state.filters = [];
|
||||
$state.$newFilters = [];
|
||||
}
|
||||
|
||||
$state.query = query;
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the user clicks "Save" button.
|
||||
*/
|
||||
|
|
|
@ -14,6 +14,16 @@ export function getUiSettingDefaults() {
|
|||
description: '<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html" target="_blank">Options</a> for the lucene query string parser',
|
||||
type: 'json'
|
||||
},
|
||||
'search:queryLanguage': {
|
||||
value: 'lucene',
|
||||
description: 'Query language used by the query bar. Kuery is an experimental new language built specifically for Kibana.',
|
||||
type: 'select',
|
||||
options: ['lucene', 'kuery']
|
||||
},
|
||||
'search:queryLanguage:switcher:enable': {
|
||||
value: false,
|
||||
description: 'Show or hide the query language switcher in the query bar'
|
||||
},
|
||||
'sort:options': {
|
||||
value: '{ "unmapped_type": "boolean" }',
|
||||
description: '<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html" target="_blank">Options</a> for the Elasticsearch sort parameter',
|
||||
|
|
10
src/test_utils/expect_deep_equal.js
Normal file
10
src/test_utils/expect_deep_equal.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { isEqual } from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
|
||||
// expect.js's `eql` method provides nice error messages but sometimes misses things
|
||||
// since it only tests loose (==) equality. This function uses lodash's `isEqual` as a
|
||||
// second sanity check since it checks for strict equality.
|
||||
export function expectDeepEqual(actual, expected) {
|
||||
expect(actual).to.eql(expected);
|
||||
expect(isEqual(actual, expected)).to.be(true);
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import { VisProvider } from 'ui/vis';
|
||||
|
@ -38,7 +36,7 @@ describe('AggConfig Filters', function () {
|
|||
|
||||
const aggConfig = vis.aggs.byTypeName.filters[0];
|
||||
const filter = createFilter(aggConfig, 'type:nginx');
|
||||
expect(_.omit(filter, 'meta')).to.eql(aggConfig.params.filters[1].input);
|
||||
expect(filter.query.query_string.query).to.be('type:nginx');
|
||||
expect(filter.meta).to.have.property('index', indexPattern.id);
|
||||
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import { luceneStringToDsl } from '../../courier/data_source/build_query/lucene_string_to_dsl.js';
|
||||
|
||||
import { AggTypesBucketsBucketAggTypeProvider } from 'ui/agg_types/buckets/_bucket_agg_type';
|
||||
import { AggTypesBucketsCreateFilterFiltersProvider } from 'ui/agg_types/buckets/create_filter/filters';
|
||||
|
@ -27,15 +28,16 @@ export function AggTypesBucketsFiltersProvider(Private, Notifier) {
|
|||
if (!_.size(inFilters)) return;
|
||||
|
||||
const outFilters = _.transform(inFilters, function (filters, filter) {
|
||||
const input = filter.input;
|
||||
const input = _.cloneDeep(filter.input);
|
||||
if (!input) return notif.log('malformed filter agg params, missing "input" query');
|
||||
|
||||
const query = input.query;
|
||||
const query = input.query = luceneStringToDsl(input.query);
|
||||
if (!query) return notif.log('malformed filter agg params, missing "query" on input');
|
||||
|
||||
decorateQuery(query);
|
||||
|
||||
const label = filter.label || _.get(query, 'query_string.query') || angular.toJson(query);
|
||||
const matchAllLabel = (filter.input.query === '' && _.has(query, 'match_all')) ? '*' : '';
|
||||
const label = filter.label || matchAllLabel || _.get(query, 'query_string.query') || angular.toJson(query);
|
||||
filters[label] = input;
|
||||
}, {});
|
||||
|
||||
|
|
|
@ -6,15 +6,15 @@ import 'ui/promises';
|
|||
import { RequestQueueProvider } from '../_request_queue';
|
||||
import { ErrorHandlersProvider } from '../_error_handlers';
|
||||
import { FetchProvider } from '../fetch';
|
||||
import { DecorateQueryProvider } from './_decorate_query';
|
||||
import { FieldWildcardProvider } from '../../field_wildcard';
|
||||
import { getHighlightRequest } from '../../../../core_plugins/kibana/common/highlight';
|
||||
import { migrateFilter } from './_migrate_filter';
|
||||
import { BuildESQueryProvider } from './build_query';
|
||||
|
||||
export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, config) {
|
||||
const requestQueue = Private(RequestQueueProvider);
|
||||
const errorHandlers = Private(ErrorHandlersProvider);
|
||||
const courierFetch = Private(FetchProvider);
|
||||
const buildESQuery = Private(BuildESQueryProvider);
|
||||
const { fieldWildcardFilter } = Private(FieldWildcardProvider);
|
||||
const getConfig = (...args) => config.get(...args);
|
||||
|
||||
|
@ -301,17 +301,8 @@ export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, con
|
|||
.then(function () {
|
||||
if (type === 'search') {
|
||||
// This is down here to prevent the circular dependency
|
||||
const decorateQuery = Private(DecorateQueryProvider);
|
||||
|
||||
flatState.body = flatState.body || {};
|
||||
|
||||
// defaults for the query
|
||||
if (!flatState.body.query) {
|
||||
flatState.body.query = {
|
||||
'match_all': {}
|
||||
};
|
||||
}
|
||||
|
||||
const computedFields = flatState.index.getComputedFields();
|
||||
flatState.body.stored_fields = computedFields.storedFields;
|
||||
flatState.body.script_fields = flatState.body.script_fields || {};
|
||||
|
@ -339,27 +330,20 @@ export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, con
|
|||
_.set(flatState.body, '_source.includes', remainingFields);
|
||||
}
|
||||
|
||||
decorateQuery(flatState.body.query);
|
||||
flatState.body.query = buildESQuery(flatState.index, flatState.query, flatState.filters);
|
||||
|
||||
if (flatState.highlightAll != null) {
|
||||
if (flatState.highlightAll && flatState.body.query) {
|
||||
flatState.body.highlight = getHighlightRequest(flatState.body.query, getConfig);
|
||||
}
|
||||
delete flatState.highlightAll;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter that can be reversed for filters with negate set
|
||||
* @param {boolean} reverse This will reverse the filter. If true then
|
||||
* anything where negate is set will come
|
||||
* through otherwise it will filter out
|
||||
* @returns {function}
|
||||
* Translate a filter into a query to support es 3+
|
||||
* @param {Object} filter - The filter to translate
|
||||
* @return {Object} the query version of that filter
|
||||
*/
|
||||
const filterNegate = function (reverse) {
|
||||
return function (filter) {
|
||||
if (_.isUndefined(filter.meta) || _.isUndefined(filter.meta.negate)) return !reverse;
|
||||
return filter.meta && filter.meta.negate === reverse;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate a filter into a query to support es 3+
|
||||
* @param {Object} filter - The filter to translate
|
||||
* @return {Object} the query version of that filter
|
||||
*/
|
||||
const translateToQuery = function (filter) {
|
||||
if (!filter) return;
|
||||
|
||||
|
@ -370,55 +354,6 @@ export function AbstractDataSourceProvider(Private, Promise, PromiseEmitter, con
|
|||
return filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean out any invalid attributes from the filters
|
||||
* @param {object} filter The filter to clean
|
||||
* @returns {object}
|
||||
*/
|
||||
const cleanFilter = function (filter) {
|
||||
return _.omit(filter, ['meta']);
|
||||
};
|
||||
|
||||
// switch to filtered query if there are filters
|
||||
if (flatState.filters) {
|
||||
if (flatState.filters.length) {
|
||||
_.each(flatState.filters, function (filter) {
|
||||
if (filter.query) {
|
||||
decorateQuery(filter.query);
|
||||
}
|
||||
});
|
||||
|
||||
flatState.body.query = {
|
||||
bool: {
|
||||
must: (
|
||||
[flatState.body.query].concat(
|
||||
(flatState.filters || [])
|
||||
.filter(filterNegate(false))
|
||||
.map(translateToQuery)
|
||||
.map(cleanFilter)
|
||||
.map(migrateFilter)
|
||||
)
|
||||
),
|
||||
must_not: (
|
||||
(flatState.filters || [])
|
||||
.filter(filterNegate(true))
|
||||
.map(translateToQuery)
|
||||
.map(cleanFilter)
|
||||
.map(migrateFilter)
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
delete flatState.filters;
|
||||
}
|
||||
|
||||
if (flatState.highlightAll != null) {
|
||||
if (flatState.highlightAll && flatState.body.query) {
|
||||
flatState.body.highlight = getHighlightRequest(flatState.body.query, getConfig);
|
||||
}
|
||||
delete flatState.highlightAll;
|
||||
}
|
||||
|
||||
// re-write filters within filter aggregations
|
||||
(function recurse(aggBranch) {
|
||||
if (!aggBranch) return;
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { BuildESQueryProvider } from '../build_es_query';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '../../../../kuery';
|
||||
import { luceneStringToDsl } from '../lucene_string_to_dsl';
|
||||
import { DecorateQueryProvider } from '../../_decorate_query';
|
||||
|
||||
let indexPattern;
|
||||
let buildEsQuery;
|
||||
let decorateQuery;
|
||||
|
||||
describe('build query', function () {
|
||||
|
||||
describe('buildESQuery', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
buildEsQuery = Private(BuildESQueryProvider);
|
||||
decorateQuery = Private(DecorateQueryProvider);
|
||||
}));
|
||||
|
||||
it('should return the parameters of an Elasticsearch bool query', function () {
|
||||
const result = buildEsQuery();
|
||||
const expected = {
|
||||
bool: {
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
}
|
||||
};
|
||||
expectDeepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('should combine queries and filters from multiple query languages into a single ES bool query', function () {
|
||||
const queries = [
|
||||
{ query: 'foo:bar', language: 'kuery' },
|
||||
{ query: 'bar:baz', language: 'lucene' },
|
||||
];
|
||||
const filters = [
|
||||
{
|
||||
match_all: {},
|
||||
meta: { type: 'match_all' },
|
||||
},
|
||||
];
|
||||
|
||||
const expectedResult = {
|
||||
bool: {
|
||||
must: [
|
||||
decorateQuery(luceneStringToDsl('bar:baz')),
|
||||
{ match_all: {} },
|
||||
],
|
||||
filter: [
|
||||
toElasticsearchQuery(fromKueryExpression('foo:bar'), indexPattern),
|
||||
],
|
||||
should: [],
|
||||
must_not: [],
|
||||
}
|
||||
};
|
||||
|
||||
const result = buildEsQuery(indexPattern, queries, filters);
|
||||
|
||||
expectDeepEqual(result, expectedResult);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
import { buildQueryFromFilters } from '../from_filters';
|
||||
import { DecorateQueryProvider } from '../../_decorate_query.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
|
||||
|
||||
let decorateQuery;
|
||||
|
||||
describe('build query', function () {
|
||||
|
||||
describe('buildQueryFromFilters', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
decorateQuery = Private(DecorateQueryProvider);
|
||||
}));
|
||||
|
||||
it('should return the parameters of an Elasticsearch bool query', function () {
|
||||
const result = buildQueryFromFilters([]);
|
||||
const expected = {
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
};
|
||||
expectDeepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('should transform an array of kibana filters into ES queries combined in the bool clauses', function () {
|
||||
const filters = [
|
||||
{
|
||||
match_all: {},
|
||||
meta: { type: 'match_all' }
|
||||
},
|
||||
{
|
||||
exists: { field: 'foo' },
|
||||
meta: { type: 'exists' }
|
||||
}
|
||||
];
|
||||
|
||||
const expectedESQueries = [
|
||||
{ match_all: {} },
|
||||
{ exists: { field: 'foo' } }
|
||||
];
|
||||
|
||||
const result = buildQueryFromFilters(filters, decorateQuery);
|
||||
|
||||
expectDeepEqual(result.must, expectedESQueries);
|
||||
});
|
||||
|
||||
it('should place negated filters in the must_not clause', function () {
|
||||
const filters = [
|
||||
{
|
||||
match_all: {},
|
||||
meta: { type: 'match_all', negate: true }
|
||||
},
|
||||
];
|
||||
|
||||
const expectedESQueries = [
|
||||
{ match_all: {} },
|
||||
];
|
||||
|
||||
const result = buildQueryFromFilters(filters, decorateQuery);
|
||||
|
||||
expectDeepEqual(result.must_not, expectedESQueries);
|
||||
});
|
||||
|
||||
it('should translate old ES filter syntax into ES 5+ query objects', function () {
|
||||
const filters = [
|
||||
{
|
||||
query: { exists: { field: 'foo' } },
|
||||
meta: { type: 'exists' }
|
||||
}
|
||||
];
|
||||
|
||||
const expectedESQueries = [
|
||||
{
|
||||
exists: { field: 'foo' }
|
||||
}
|
||||
];
|
||||
|
||||
const result = buildQueryFromFilters(filters, decorateQuery);
|
||||
|
||||
expectDeepEqual(result.must, expectedESQueries);
|
||||
});
|
||||
|
||||
it('should migrate deprecated match syntax', function () {
|
||||
const filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'foo', type: 'phrase' } } },
|
||||
meta: { type: 'phrase' }
|
||||
}
|
||||
];
|
||||
|
||||
const expectedESQueries = [
|
||||
{
|
||||
match_phrase: { extension: { query: 'foo' } },
|
||||
}
|
||||
];
|
||||
|
||||
const result = buildQueryFromFilters(filters, decorateQuery);
|
||||
|
||||
expectDeepEqual(result.must, expectedESQueries);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { buildQueryFromKuery } from '../from_kuery';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '../../../../kuery';
|
||||
|
||||
let indexPattern;
|
||||
|
||||
describe('build query', function () {
|
||||
|
||||
describe('buildQueryFromKuery', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
it('should return the parameters of an Elasticsearch bool query', function () {
|
||||
const result = buildQueryFromKuery();
|
||||
const expected = {
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
};
|
||||
expectDeepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('should transform an array of kuery queries into ES queries combined in the bool\'s filter clause', function () {
|
||||
const queries = [
|
||||
{ query: 'foo:bar', language: 'kuery' },
|
||||
{ query: 'bar:baz', language: 'kuery' },
|
||||
];
|
||||
|
||||
const expectedESQueries = queries.map(
|
||||
(query) => {
|
||||
return toElasticsearchQuery(fromKueryExpression(query.query), indexPattern);
|
||||
}
|
||||
);
|
||||
|
||||
const result = buildQueryFromKuery(indexPattern, queries);
|
||||
|
||||
expectDeepEqual(result.filter, expectedESQueries);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
import { buildQueryFromLucene } from '../from_lucene';
|
||||
import { DecorateQueryProvider } from '../../_decorate_query.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
|
||||
import { luceneStringToDsl } from '../lucene_string_to_dsl';
|
||||
|
||||
let decorateQuery;
|
||||
|
||||
describe('build query', function () {
|
||||
|
||||
describe('buildQueryFromLucene', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
decorateQuery = Private(DecorateQueryProvider);
|
||||
}));
|
||||
|
||||
it('should return the parameters of an Elasticsearch bool query', function () {
|
||||
const result = buildQueryFromLucene();
|
||||
const expected = {
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
};
|
||||
expectDeepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('should transform an array of lucene queries into ES queries combined in the bool\'s must clause', function () {
|
||||
const queries = [
|
||||
{ query: 'foo:bar', language: 'lucene' },
|
||||
{ query: 'bar:baz', language: 'lucene' },
|
||||
];
|
||||
|
||||
const expectedESQueries = queries.map(
|
||||
(query) => {
|
||||
return decorateQuery(luceneStringToDsl(query.query));
|
||||
}
|
||||
);
|
||||
|
||||
const result = buildQueryFromLucene(queries, decorateQuery);
|
||||
|
||||
expectDeepEqual(result.must, expectedESQueries);
|
||||
});
|
||||
|
||||
it('should also accept queries in ES query DSL format, simply passing them through', function () {
|
||||
const queries = [
|
||||
{ query: { match_all: {} }, language: 'lucene' },
|
||||
];
|
||||
|
||||
const result = buildQueryFromLucene(queries, decorateQuery);
|
||||
|
||||
expectDeepEqual(result.must, [queries[0].query]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { luceneStringToDsl } from '../lucene_string_to_dsl';
|
||||
import { expectDeepEqual } from '../../../../../../test_utils/expect_deep_equal.js';
|
||||
import expect from 'expect.js';
|
||||
|
||||
describe('build query', function () {
|
||||
|
||||
describe('luceneStringToDsl', function () {
|
||||
|
||||
it('should wrap strings with an ES query_string query', function () {
|
||||
const result = luceneStringToDsl('foo:bar');
|
||||
const expectedResult = {
|
||||
query_string: { query: 'foo:bar' }
|
||||
};
|
||||
expectDeepEqual(result, expectedResult);
|
||||
});
|
||||
|
||||
it('should return a match_all query for empty strings and whitespace', function () {
|
||||
const expectedResult = {
|
||||
match_all: {}
|
||||
};
|
||||
|
||||
expectDeepEqual(luceneStringToDsl(''), expectedResult);
|
||||
expectDeepEqual(luceneStringToDsl(' '), expectedResult);
|
||||
});
|
||||
|
||||
it('should return non-string arguments without modification', function () {
|
||||
const expectedResult = {};
|
||||
const result = luceneStringToDsl(expectedResult);
|
||||
expect(result).to.be(expectedResult);
|
||||
expectDeepEqual(result, expectedResult);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { groupBy, has } from 'lodash';
|
||||
import { DecorateQueryProvider } from '../_decorate_query';
|
||||
import { buildQueryFromKuery } from './from_kuery';
|
||||
import { buildQueryFromFilters } from './from_filters';
|
||||
import { buildQueryFromLucene } from './from_lucene';
|
||||
|
||||
export function BuildESQueryProvider(Private) {
|
||||
const decorateQuery = Private(DecorateQueryProvider);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param queries - an array of query objects. Each query has a language property and a query property.
|
||||
* @param filters - an array of filter objects
|
||||
*/
|
||||
function buildESQuery(indexPattern, queries = [], filters = []) {
|
||||
const validQueries = queries.filter((query) => has(query, 'query'));
|
||||
const queriesByLanguage = groupBy(validQueries, 'language');
|
||||
|
||||
const kueryQuery = buildQueryFromKuery(indexPattern, queriesByLanguage.kuery);
|
||||
const luceneQuery = buildQueryFromLucene(queriesByLanguage.lucene, decorateQuery);
|
||||
const filterQuery = buildQueryFromFilters(filters, decorateQuery);
|
||||
|
||||
return {
|
||||
bool: {
|
||||
must: [].concat(kueryQuery.must, luceneQuery.must, filterQuery.must),
|
||||
filter: [].concat(kueryQuery.filter, luceneQuery.filter, filterQuery.filter),
|
||||
should: [].concat(kueryQuery.should, luceneQuery.should, filterQuery.should),
|
||||
must_not: [].concat(kueryQuery.must_not, luceneQuery.must_not, filterQuery.must_not),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return buildESQuery;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import _ from 'lodash';
|
||||
import { migrateFilter } from '../_migrate_filter';
|
||||
|
||||
/**
|
||||
* Create a filter that can be reversed for filters with negate set
|
||||
* @param {boolean} reverse This will reverse the filter. If true then
|
||||
* anything where negate is set will come
|
||||
* through otherwise it will filter out
|
||||
* @returns {function}
|
||||
*/
|
||||
const filterNegate = function (reverse) {
|
||||
return function (filter) {
|
||||
if (_.isUndefined(filter.meta) || _.isUndefined(filter.meta.negate)) return !reverse;
|
||||
return filter.meta && filter.meta.negate === reverse;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Translate a filter into a query to support es 5+
|
||||
* @param {Object} filter - The filter to translate
|
||||
* @return {Object} the query version of that filter
|
||||
*/
|
||||
const translateToQuery = function (filter) {
|
||||
if (!filter) return;
|
||||
|
||||
if (filter.query) {
|
||||
return filter.query;
|
||||
}
|
||||
|
||||
return filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Clean out any invalid attributes from the filters
|
||||
* @param {object} filter The filter to clean
|
||||
* @returns {object}
|
||||
*/
|
||||
const cleanFilter = function (filter) {
|
||||
return _.omit(filter, ['meta']);
|
||||
};
|
||||
|
||||
export function buildQueryFromFilters(filters, decorateQuery) {
|
||||
_.each(filters, function (filter) {
|
||||
if (filter.query) {
|
||||
decorateQuery(filter.query);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
must: (filters || [])
|
||||
.filter(filterNegate(false))
|
||||
.map(translateToQuery)
|
||||
.map(cleanFilter)
|
||||
.map(migrateFilter),
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: (filters || [])
|
||||
.filter(filterNegate(true))
|
||||
.map(translateToQuery)
|
||||
.map(cleanFilter)
|
||||
.map(migrateFilter)
|
||||
};
|
||||
}
|
16
src/ui/public/courier/data_source/build_query/from_kuery.js
Normal file
16
src/ui/public/courier/data_source/build_query/from_kuery.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import _ from 'lodash';
|
||||
import { fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../../../kuery';
|
||||
|
||||
export function buildQueryFromKuery(indexPattern, queries) {
|
||||
const queryASTs = _.map(queries, query => fromKueryExpression(query.query));
|
||||
const compoundQueryAST = nodeTypes.function.buildNode('and', queryASTs);
|
||||
const kueryQuery = toElasticsearchQuery(compoundQueryAST, indexPattern);
|
||||
return Object.assign({
|
||||
must: [],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
}, kueryQuery.bool);
|
||||
}
|
||||
|
||||
|
16
src/ui/public/courier/data_source/build_query/from_lucene.js
Normal file
16
src/ui/public/courier/data_source/build_query/from_lucene.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import _ from 'lodash';
|
||||
import { luceneStringToDsl } from './lucene_string_to_dsl';
|
||||
|
||||
export function buildQueryFromLucene(queries, decorateQuery) {
|
||||
const combinedQueries = _.map(queries, (query) => {
|
||||
const queryDsl = luceneStringToDsl(query.query);
|
||||
return decorateQuery(queryDsl);
|
||||
});
|
||||
|
||||
return {
|
||||
must: [].concat(combinedQueries),
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
};
|
||||
}
|
1
src/ui/public/courier/data_source/build_query/index.js
Normal file
1
src/ui/public/courier/data_source/build_query/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { BuildESQueryProvider } from './build_es_query';
|
|
@ -0,0 +1,13 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
export function luceneStringToDsl(query) {
|
||||
if (!_.isString(query)) {
|
||||
return query;
|
||||
}
|
||||
|
||||
if (query.trim() === '') {
|
||||
return { match_all: {} };
|
||||
}
|
||||
|
||||
return { query_string: { query } };
|
||||
}
|
|
@ -269,6 +269,9 @@ export function SearchSourceProvider(Promise, Private, config) {
|
|||
val = normalizeSortRequest(val, this.get('index'));
|
||||
addToBody();
|
||||
break;
|
||||
case 'query':
|
||||
state.query = (state.query || []).concat(val);
|
||||
break;
|
||||
case 'fields':
|
||||
state[key] = _.uniq([...(state[key] || []), ...val]);
|
||||
break;
|
||||
|
@ -283,10 +286,6 @@ export function SearchSourceProvider(Promise, Private, config) {
|
|||
state.body = state.body || {};
|
||||
// ignore if we already have a value
|
||||
if (state.body[key] == null) {
|
||||
if (key === 'query' && _.isString(val)) {
|
||||
val = { query_string: { query: val } };
|
||||
}
|
||||
|
||||
state.body[key] = val;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import MappingSetupProvider from 'ui/utils/mapping_setup';
|
|||
|
||||
import { SearchSourceProvider } from '../data_source/search_source';
|
||||
import { SavedObjectsClientProvider, findObjectByTitle } from 'ui/saved_objects';
|
||||
import { migrateLegacyQuery } from '../../utils/migrateLegacyQuery.js';
|
||||
|
||||
/**
|
||||
* An error message to be used when the user rejects a confirm overwrite.
|
||||
|
@ -105,6 +106,8 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
|
|||
}, {});
|
||||
|
||||
this.searchSource.set(_.defaults(state, fnProps));
|
||||
|
||||
this.searchSource.set('query', migrateLegacyQuery(this.searchSource.getOwn('query')));
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,29 +66,21 @@ describe('parse-query directive', function () {
|
|||
expect(fromUser({ foo: 'bar' })).to.eql({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('unless the object is empty, that implies a *', function () {
|
||||
expect(fromUser({})).to.eql({ query_string: { query: '*' } });
|
||||
it('unless the object is empty, then convert it to an empty string', function () {
|
||||
expect(fromUser({})).to.eql('');
|
||||
});
|
||||
|
||||
it('should treat an empty string as a *', function () {
|
||||
expect(fromUser('')).to.eql({ query_string: { query: '*' } });
|
||||
it('should pass through input strings that not start with {', function () {
|
||||
expect(fromUser('foo')).to.eql('foo');
|
||||
expect(fromUser('400')).to.eql('400');
|
||||
expect(fromUser('true')).to.eql('true');
|
||||
});
|
||||
|
||||
it('should merge in the query string options', function () {
|
||||
config.set('query:queryString:options', { analyze_wildcard: true });
|
||||
expect(fromUser('foo')).to.eql({ query_string: { query: 'foo', analyze_wildcard: true } });
|
||||
expect(fromUser('')).to.eql({ query_string: { query: '*', analyze_wildcard: true } });
|
||||
});
|
||||
|
||||
it('should treat input that does not start with { as a query string', function () {
|
||||
expect(fromUser('foo')).to.eql({ query_string: { query: 'foo' } });
|
||||
expect(fromUser('400')).to.eql({ query_string: { query: '400' } });
|
||||
expect(fromUser('true')).to.eql({ query_string: { query: 'true' } });
|
||||
});
|
||||
|
||||
it('should parse valid JSON', function () {
|
||||
it('should parse valid JSON and return the object instead of a string', function () {
|
||||
expect(fromUser('{}')).to.eql({});
|
||||
expect(fromUser('{a:b}')).to.eql({ query_string: { query: '{a:b}' } });
|
||||
|
||||
// invalid json remains a string
|
||||
expect(fromUser('{a:b}')).to.eql('{a:b}');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ describe('Filter Bar pushFilter()', function () {
|
|||
let filter;
|
||||
|
||||
beforeEach(ngMock.inject(function () {
|
||||
$state = { filters:[] };
|
||||
$state = { $newFilters:[] };
|
||||
pushFilter = pushFilterFn($state);
|
||||
filter = { query: { query_string: { query: '' } } };
|
||||
}));
|
||||
|
@ -33,7 +33,7 @@ describe('Filter Bar pushFilter()', function () {
|
|||
it('should create the filters property it needed', function () {
|
||||
const altState = {};
|
||||
pushFilterFn(altState)(filter);
|
||||
expect(altState.filters).to.be.an(Array);
|
||||
expect(altState.$newFilters).to.be.an(Array);
|
||||
});
|
||||
|
||||
it('should replace the filters property instead of modifying it', function () {
|
||||
|
@ -41,24 +41,24 @@ describe('Filter Bar pushFilter()', function () {
|
|||
|
||||
let oldFilters;
|
||||
|
||||
oldFilters = $state.filters;
|
||||
$state.filters.push(filter);
|
||||
expect($state.filters).to.equal(oldFilters); // Same object
|
||||
oldFilters = $state.$newFilters;
|
||||
$state.$newFilters.push(filter);
|
||||
expect($state.$newFilters).to.equal(oldFilters); // Same object
|
||||
|
||||
oldFilters = $state.filters;
|
||||
oldFilters = $state.$newFilters;
|
||||
pushFilter(filter);
|
||||
expect($state.filters).to.not.equal(oldFilters); // New object!
|
||||
expect($state.$newFilters).to.not.equal(oldFilters); // New object!
|
||||
});
|
||||
|
||||
it('should add meta data to the filter', function () {
|
||||
pushFilter(filter, true, 'myIndex');
|
||||
expect($state.filters[0].meta).to.be.an(Object);
|
||||
expect($state.$newFilters[0].meta).to.be.an(Object);
|
||||
|
||||
expect($state.filters[0].meta.negate).to.be(true);
|
||||
expect($state.filters[0].meta.index).to.be('myIndex');
|
||||
expect($state.$newFilters[0].meta.negate).to.be(true);
|
||||
expect($state.$newFilters[0].meta.index).to.be('myIndex');
|
||||
|
||||
pushFilter(filter, false, 'myIndex');
|
||||
expect($state.filters[1].meta.negate).to.be(false);
|
||||
expect($state.$newFilters[0].meta.negate).to.be(false);
|
||||
});
|
||||
|
||||
|
||||
|
|
82
src/ui/public/filter_bar/lib/__tests__/map_geo_polygon.js
Normal file
82
src/ui/public/filter_bar/lib/__tests__/map_geo_polygon.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import { FilterBarLibMapGeoPolygonProvider } from 'ui/filter_bar/lib/map_geo_polygon';
|
||||
|
||||
describe('Filter Bar Directive', function () {
|
||||
describe('mapGeoPolygon()', function () {
|
||||
let mapGeoPolygon;
|
||||
let $rootScope;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
function ($provide) {
|
||||
$provide.service('courier', require('fixtures/mock_courier'));
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (Private, _$rootScope_) {
|
||||
mapGeoPolygon = Private(FilterBarLibMapGeoPolygonProvider);
|
||||
$rootScope = _$rootScope_;
|
||||
}));
|
||||
|
||||
it('should return the key and value for matching filters with bounds', function (done) {
|
||||
const filter = {
|
||||
meta: {
|
||||
index: 'logstash-*'
|
||||
},
|
||||
geo_polygon: {
|
||||
point: { // field name
|
||||
points: [
|
||||
{ lat: 5, lon: 10 },
|
||||
{ lat: 15, lon: 20 }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
mapGeoPolygon(filter).then(function (result) {
|
||||
expect(result).to.have.property('key', 'point');
|
||||
expect(result).to.have.property('value');
|
||||
// remove html entities and non-alphanumerics to get the gist of the value
|
||||
expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).to.be('lat5lon10lat15lon20');
|
||||
done();
|
||||
});
|
||||
$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return undefined for none matching', function (done) {
|
||||
const filter = { meta: { index: 'logstash-*' }, query: { query_string: { query: 'foo:bar' } } };
|
||||
mapGeoPolygon(filter).catch(function (result) {
|
||||
expect(result).to.be(filter);
|
||||
done();
|
||||
});
|
||||
$rootScope.$apply();
|
||||
});
|
||||
|
||||
it('should return the key and value even when using ignore_unmapped', function (done) {
|
||||
const filter = {
|
||||
meta: {
|
||||
index: 'logstash-*'
|
||||
},
|
||||
geo_polygon: {
|
||||
ignore_unmapped: true,
|
||||
point: { // field name
|
||||
points: [
|
||||
{ lat: 5, lon: 10 },
|
||||
{ lat: 15, lon: 20 }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
mapGeoPolygon(filter).then(function (result) {
|
||||
expect(result).to.have.property('key', 'point');
|
||||
expect(result).to.have.property('value');
|
||||
// remove html entities and non-alphanumerics to get the gist of the value
|
||||
expect(result.value.replace(/&[a-z]+?;/g, '').replace(/[^a-z0-9]/g, '')).to.be('lat5lon10lat15lon20');
|
||||
done();
|
||||
});
|
||||
$rootScope.$apply();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@ import { FilterBarLibMapExistsProvider } from './map_exists';
|
|||
import { FilterBarLibMapMissingProvider } from './map_missing';
|
||||
import { FilterBarLibMapQueryStringProvider } from './map_query_string';
|
||||
import { FilterBarLibMapGeoBoundingBoxProvider } from './map_geo_bounding_box';
|
||||
import { FilterBarLibMapGeoPolygonProvider } from './map_geo_polygon';
|
||||
import { FilterBarLibMapScriptProvider } from './map_script';
|
||||
import { FilterBarLibMapDefaultProvider } from './map_default';
|
||||
|
||||
|
@ -40,8 +41,9 @@ export function FilterBarLibMapFilterProvider(Promise, Private) {
|
|||
Private(FilterBarLibMapMissingProvider),
|
||||
Private(FilterBarLibMapQueryStringProvider),
|
||||
Private(FilterBarLibMapGeoBoundingBoxProvider),
|
||||
Private(FilterBarLibMapGeoPolygonProvider),
|
||||
Private(FilterBarLibMapScriptProvider),
|
||||
Private(FilterBarLibMapDefaultProvider)
|
||||
Private(FilterBarLibMapDefaultProvider),
|
||||
];
|
||||
|
||||
const noop = function () {
|
||||
|
@ -68,6 +70,7 @@ export function FilterBarLibMapFilterProvider(Promise, Private) {
|
|||
filter.meta.type = result.type;
|
||||
filter.meta.key = result.key;
|
||||
filter.meta.value = result.value;
|
||||
filter.meta.params = result.params;
|
||||
filter.meta.disabled = !!(filter.meta.disabled);
|
||||
filter.meta.negate = !!(filter.meta.negate);
|
||||
filter.meta.alias = filter.meta.alias || null;
|
||||
|
|
|
@ -10,11 +10,11 @@ export function FilterBarLibMapGeoBoundingBoxProvider(Promise, courier) {
|
|||
const key = _.keys(filter.geo_bounding_box)
|
||||
.filter(key => key !== 'ignore_unmapped')[0];
|
||||
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 params = filter.geo_bounding_box[key];
|
||||
const topLeft = field.format.convert(params.top_left);
|
||||
const bottomRight = field.format.convert(params.bottom_right);
|
||||
const value = topLeft + ' to ' + bottomRight;
|
||||
return { type, key, value };
|
||||
return { type, key, value, params };
|
||||
});
|
||||
}
|
||||
return Promise.reject(filter);
|
||||
|
|
21
src/ui/public/filter_bar/lib/map_geo_polygon.js
Normal file
21
src/ui/public/filter_bar/lib/map_geo_polygon.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
export function FilterBarLibMapGeoPolygonProvider(Promise, courier) {
|
||||
return function (filter) {
|
||||
if (filter.geo_polygon) {
|
||||
return courier
|
||||
.indexPatterns
|
||||
.get(filter.meta.index).then(function (indexPattern) {
|
||||
const type = 'geo_polygon';
|
||||
const key = _.keys(filter.geo_polygon)
|
||||
.filter(key => key !== 'ignore_unmapped')[0];
|
||||
const field = indexPattern.fields.byName[key];
|
||||
const params = filter.geo_polygon[key];
|
||||
const points = params.points.map((point) => field.format.convert(point));
|
||||
const value = points.join(', ');
|
||||
return { type, key, value, params };
|
||||
});
|
||||
}
|
||||
return Promise.reject(filter);
|
||||
};
|
||||
}
|
|
@ -13,9 +13,10 @@ export function FilterBarLibMapPhraseProvider(Promise, courier) {
|
|||
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 params = isScriptedPhraseFilter ? filter.script.script.params : filter.query.match[key];
|
||||
const query = isScriptedPhraseFilter ? params.value : params.query;
|
||||
const value = field.format.convert(query);
|
||||
return { type, key, value };
|
||||
return { type, key, value, params };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,17 +14,17 @@ export function FilterBarLibMapRangeProvider(Promise, courier) {
|
|||
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 = isScriptedRangeFilter ? filter.script.script.params : filter.range[key];
|
||||
const params = isScriptedRangeFilter ? filter.script.script.params : filter.range[key];
|
||||
|
||||
let left = has(range, 'gte') ? range.gte : range.gt;
|
||||
let left = has(params, 'gte') ? params.gte : params.gt;
|
||||
if (left == null) left = -Infinity;
|
||||
|
||||
let right = has(range, 'lte') ? range.lte : range.lt;
|
||||
let right = has(params, 'lte') ? params.lte : params.lt;
|
||||
if (right == null) right = Infinity;
|
||||
|
||||
const value = `${convert(left)} to ${convert(right)}`;
|
||||
|
||||
return { type, key, value };
|
||||
return { type, key, value, params };
|
||||
});
|
||||
|
||||
};
|
||||
|
|
|
@ -7,11 +7,9 @@ export function FilterBarPushFilterProvider() {
|
|||
// Hierarchical and tabular data set their aggConfigResult parameter
|
||||
// differently because of how the point is rewritten between the two. So
|
||||
// we need to check if the point.orig is set, if not use try the point.aggConfigResult
|
||||
const filters = _.clone($state.filters || []);
|
||||
const pendingFilter = { meta: { negate: negate, index: index } };
|
||||
_.extend(pendingFilter, filter);
|
||||
filters.push(pendingFilter);
|
||||
$state.filters = filters;
|
||||
$state.$newFilters = [pendingFilter];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,41 +28,7 @@ export function buildRangeFilter(field, params, indexPattern, formattedValue) {
|
|||
filter.match_all = {};
|
||||
filter.meta.field = field.name;
|
||||
} else if (field.scripted) {
|
||||
const operators = {
|
||||
gt: '>',
|
||||
gte: '>=',
|
||||
lte: '<=',
|
||||
lt: '<',
|
||||
};
|
||||
const comparators = {
|
||||
gt: 'boolean gt(Supplier s, def v) {return s.get() > v}',
|
||||
gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}',
|
||||
lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}',
|
||||
lt: 'boolean lt(Supplier s, def v) {return s.get() < v}',
|
||||
};
|
||||
|
||||
const knownParams = _.pick(params, (val, key) => { return key in operators; });
|
||||
let script = _.map(knownParams, function (val, key) {
|
||||
return '(' + field.script + ')' + operators[key] + key;
|
||||
}).join(' && ');
|
||||
|
||||
// We must wrap painless scripts in a lambda in case they're more than a simple expression
|
||||
if (field.lang === 'painless') {
|
||||
const currentComparators = _.reduce(knownParams, (acc, val, key) => acc.concat(comparators[key]), []).join(' ');
|
||||
|
||||
const comparisons = _.map(knownParams, function (val, key) {
|
||||
return `${key}(() -> { ${field.script} }, params.${key})`;
|
||||
}).join(' && ');
|
||||
|
||||
script = `${currentComparators}${comparisons}`;
|
||||
}
|
||||
|
||||
const value = _.map(knownParams, function (val, key) {
|
||||
return operators[key] + field.format.convert(val);
|
||||
}).join(' ');
|
||||
|
||||
_.set(filter, 'script.script', { inline: script, params: knownParams, lang: field.lang });
|
||||
filter.script.script.params.value = value;
|
||||
filter.script = getRangeScript(field, params);
|
||||
filter.meta.field = field.name;
|
||||
} else {
|
||||
filter.range = {};
|
||||
|
@ -71,3 +37,51 @@ export function buildRangeFilter(field, params, indexPattern, formattedValue) {
|
|||
|
||||
return filter;
|
||||
}
|
||||
|
||||
export function getRangeScript(field, params) {
|
||||
const operators = {
|
||||
gt: '>',
|
||||
gte: '>=',
|
||||
lte: '<=',
|
||||
lt: '<',
|
||||
};
|
||||
const comparators = {
|
||||
gt: 'boolean gt(Supplier s, def v) {return s.get() > v}',
|
||||
gte: 'boolean gte(Supplier s, def v) {return s.get() >= v}',
|
||||
lte: 'boolean lte(Supplier s, def v) {return s.get() <= v}',
|
||||
lt: 'boolean lt(Supplier s, def v) {return s.get() < v}',
|
||||
};
|
||||
|
||||
const knownParams = _.pick(params, (val, key) => {
|
||||
return key in operators;
|
||||
});
|
||||
let script = _.map(knownParams, function (val, key) {
|
||||
return '(' + field.script + ')' + operators[key] + key;
|
||||
}).join(' && ');
|
||||
|
||||
// We must wrap painless scripts in a lambda in case they're more than a simple expression
|
||||
if (field.lang === 'painless') {
|
||||
const currentComparators = _.reduce(knownParams, (acc, val, key) => acc.concat(comparators[key]), []).join(' ');
|
||||
|
||||
const comparisons = _.map(knownParams, function (val, key) {
|
||||
return `${key}(() -> { ${field.script} }, params.${key})`;
|
||||
}).join(' && ');
|
||||
|
||||
script = `${currentComparators}${comparisons}`;
|
||||
}
|
||||
|
||||
const value = _.map(knownParams, function (val, key) {
|
||||
return operators[key] + field.format.convert(val);
|
||||
}).join(' ');
|
||||
|
||||
return {
|
||||
script: {
|
||||
inline: script,
|
||||
params: {
|
||||
...knownParams,
|
||||
value,
|
||||
},
|
||||
lang: field.lang
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
253
src/ui/public/kuery/ast/__tests__/ast.js
Normal file
253
src/ui/public/kuery/ast/__tests__/ast.js
Normal file
|
@ -0,0 +1,253 @@
|
|||
import * as ast from '../ast';
|
||||
import expect from 'expect.js';
|
||||
import { nodeTypes } from '../../node_types/index';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
|
||||
|
||||
|
||||
// Helpful utility allowing us to test the PEG parser by simply checking for deep equality between
|
||||
// the nodes the parser generates and the nodes our constructor functions generate.
|
||||
function fromKueryExpressionNoMeta(text) {
|
||||
return ast.fromKueryExpression(text, { includeMetadata: false });
|
||||
}
|
||||
|
||||
let indexPattern;
|
||||
|
||||
describe('kuery AST API', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('fromKueryExpression', function () {
|
||||
|
||||
it('should return location and text metadata for each AST node', function () {
|
||||
const notNode = ast.fromKueryExpression('!foo:bar');
|
||||
expect(notNode).to.have.property('text', '!foo:bar');
|
||||
expect(notNode.location).to.eql({ min: 0, max: 8 });
|
||||
|
||||
const isNode = notNode.arguments[0];
|
||||
expect(isNode).to.have.property('text', 'foo:bar');
|
||||
expect(isNode.location).to.eql({ min: 1, max: 8 });
|
||||
|
||||
const { arguments: [ argNode1, argNode2 ] } = isNode;
|
||||
expect(argNode1).to.have.property('text', 'foo');
|
||||
expect(argNode1.location).to.eql({ min: 1, max: 4 });
|
||||
|
||||
expect(argNode2).to.have.property('text', 'bar');
|
||||
expect(argNode2.location).to.eql({ min: 5, max: 8 });
|
||||
});
|
||||
|
||||
it('should return a match all "is" function for whitespace', function () {
|
||||
const expected = nodeTypes.function.buildNode('is', '*', '*');
|
||||
const actual = fromKueryExpressionNoMeta(' ');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should return an "and" function for single literals', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')], 'implicit');
|
||||
const actual = fromKueryExpressionNoMeta('foo');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should ignore extraneous whitespace at the beginning and end of the query', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')], 'implicit');
|
||||
const actual = fromKueryExpressionNoMeta(' foo ');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('literals and queries separated by whitespace should be joined by an implicit "and"', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'implicit');
|
||||
const actual = fromKueryExpressionNoMeta('foo bar');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should also support explicit "and"s as a binary operator', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'operator');
|
||||
const actual = fromKueryExpressionNoMeta('foo and bar');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should also support "and" as a function', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'function');
|
||||
const actual = fromKueryExpressionNoMeta('and(foo, bar)');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should support "or" as a binary operator', function () {
|
||||
const expected = nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'operator');
|
||||
const actual = fromKueryExpressionNoMeta('foo or bar');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should support "or" as a function', function () {
|
||||
const expected = nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'function');
|
||||
const actual = fromKueryExpressionNoMeta('or(foo, bar)');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should support negation of queries with a "!" prefix', function () {
|
||||
const expected = nodeTypes.function.buildNode('not',
|
||||
nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'function'), 'operator');
|
||||
const actual = fromKueryExpressionNoMeta('!or(foo, bar)');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('"and" should have a higher precedence than "or"', function () {
|
||||
const expected = nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
nodeTypes.literal.buildNode('baz'),
|
||||
], 'operator'),
|
||||
nodeTypes.literal.buildNode('qux'),
|
||||
])
|
||||
], 'operator');
|
||||
const actual = fromKueryExpressionNoMeta('foo or bar and baz or qux');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should support grouping to override default precedence', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'operator'),
|
||||
nodeTypes.literal.buildNode('baz'),
|
||||
], 'operator');
|
||||
const actual = fromKueryExpressionNoMeta('(foo or bar) and baz');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should support a shorthand operator syntax for "is" functions', function () {
|
||||
const expected = nodeTypes.function.buildNode('is', 'foo', 'bar', 'operator');
|
||||
const actual = fromKueryExpressionNoMeta('foo:bar');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should support a shorthand operator syntax for inclusive "range" functions', function () {
|
||||
const argumentNodes = [
|
||||
nodeTypes.literal.buildNode('bytes'),
|
||||
nodeTypes.literal.buildNode(1000),
|
||||
nodeTypes.literal.buildNode(8000),
|
||||
];
|
||||
const expected = nodeTypes.function.buildNodeWithArgumentNodes('range', argumentNodes, 'operator');
|
||||
const actual = fromKueryExpressionNoMeta('bytes:[1000 to 8000]');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should support functions with named arguments', function () {
|
||||
const expected = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 }, 'function');
|
||||
const actual = fromKueryExpressionNoMeta('range(bytes, gt=1000, lt=8000)');
|
||||
expectDeepEqual(actual, expected);
|
||||
});
|
||||
|
||||
it('should throw an error for unknown functions', function () {
|
||||
expect(ast.fromKueryExpression).withArgs('foo(bar)').to.throwException(/Unknown function "foo"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should return the given node type\'s kuery string representation', function () {
|
||||
const node = nodeTypes.function.buildNode('exists', 'foo');
|
||||
const expected = nodeTypes.function.toKueryExpression(node);
|
||||
const result = ast.toKueryExpression(node);
|
||||
expectDeepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('should return an empty string for undefined nodes and unknown node types', function () {
|
||||
expect(ast.toKueryExpression()).to.be('');
|
||||
|
||||
const noTypeNode = nodeTypes.function.buildNode('exists', 'foo');
|
||||
delete noTypeNode.type;
|
||||
expect(ast.toKueryExpression(noTypeNode)).to.be('');
|
||||
|
||||
const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo');
|
||||
unknownTypeNode.type = 'notValid';
|
||||
expect(ast.toKueryExpression(unknownTypeNode)).to.be('');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return the given node type\'s ES query representation', function () {
|
||||
const node = nodeTypes.function.buildNode('exists', 'response');
|
||||
const expected = nodeTypes.function.toElasticsearchQuery(node, indexPattern);
|
||||
const result = ast.toElasticsearchQuery(node, indexPattern);
|
||||
expectDeepEqual(result, expected);
|
||||
});
|
||||
|
||||
it('should return an empty "and" function for undefined nodes and unknown node types', function () {
|
||||
const expected = nodeTypes.function.toElasticsearchQuery(nodeTypes.function.buildNode('and', []));
|
||||
|
||||
expectDeepEqual(ast.toElasticsearchQuery(), expected);
|
||||
|
||||
const noTypeNode = nodeTypes.function.buildNode('exists', 'foo');
|
||||
delete noTypeNode.type;
|
||||
expectDeepEqual(ast.toElasticsearchQuery(noTypeNode), expected);
|
||||
|
||||
const unknownTypeNode = nodeTypes.function.buildNode('exists', 'foo');
|
||||
unknownTypeNode.type = 'notValid';
|
||||
expectDeepEqual(ast.toElasticsearchQuery(unknownTypeNode), expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('symmetry of to/fromKueryExpression', function () {
|
||||
|
||||
it('toKueryExpression and fromKueryExpression should be inverse operations', function () {
|
||||
function testExpression(expression) {
|
||||
expect(ast.toKueryExpression(ast.fromKueryExpression(expression))).to.be(expression);
|
||||
}
|
||||
|
||||
testExpression('');
|
||||
testExpression(' ');
|
||||
testExpression('foo');
|
||||
testExpression('foo bar');
|
||||
testExpression('foo 200');
|
||||
testExpression('bytes:[1000 to 8000]');
|
||||
testExpression('bytes:[1000 TO 8000]');
|
||||
testExpression('range(bytes, gt=1000, lt=8000)');
|
||||
testExpression('range(bytes, gt=1000, lte=8000)');
|
||||
testExpression('range(bytes, gte=1000, lt=8000)');
|
||||
testExpression('range(bytes, gte=1000, lte=8000)');
|
||||
testExpression('response:200');
|
||||
testExpression('"response":200');
|
||||
testExpression('response:"200"');
|
||||
testExpression('"response":"200"');
|
||||
testExpression('is(response, 200)');
|
||||
testExpression('!is(response, 200)');
|
||||
testExpression('foo or is(tic, tock) or foo:bar');
|
||||
testExpression('or(foo, is(tic, tock), foo:bar)');
|
||||
testExpression('foo is(tic, tock) foo:bar');
|
||||
testExpression('foo and is(tic, tock) and foo:bar');
|
||||
testExpression('(foo or is(tic, tock)) and foo:bar');
|
||||
testExpression('!(foo or is(tic, tock)) and foo:bar');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
32
src/ui/public/kuery/ast/ast.js
Normal file
32
src/ui/public/kuery/ast/ast.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import grammar from 'raw!./kuery.peg';
|
||||
import PEG from 'pegjs';
|
||||
import _ from 'lodash';
|
||||
import { nodeTypes } from '../node_types/index';
|
||||
|
||||
const kueryParser = PEG.buildParser(grammar);
|
||||
|
||||
export function fromKueryExpression(expression, parseOptions = {}) {
|
||||
if (_.isUndefined(expression)) {
|
||||
throw new Error('expression must be a string, got undefined instead');
|
||||
}
|
||||
|
||||
parseOptions = Object.assign({}, parseOptions, { helpers: { nodeTypes } });
|
||||
|
||||
return kueryParser.parse(expression, parseOptions);
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
if (!node || !node.type || !nodeTypes[node.type]) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return nodeTypes[node.type].toKueryExpression(node);
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
if (!node || !node.type || !nodeTypes[node.type]) {
|
||||
return toElasticsearchQuery(nodeTypes.function.buildNode('and', []));
|
||||
}
|
||||
|
||||
return nodeTypes[node.type].toElasticsearchQuery(node, indexPattern);
|
||||
}
|
1
src/ui/public/kuery/ast/index.js
Normal file
1
src/ui/public/kuery/ast/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { fromKueryExpression, toKueryExpression, toElasticsearchQuery } from './ast';
|
150
src/ui/public/kuery/ast/kuery.peg
Normal file
150
src/ui/public/kuery/ast/kuery.peg
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Kuery parser
|
||||
*/
|
||||
|
||||
/*
|
||||
* Initialization block
|
||||
*/
|
||||
{
|
||||
var nodeTypes = options.helpers.nodeTypes;
|
||||
|
||||
if (options.includeMetadata === undefined) {
|
||||
options.includeMetadata = true;
|
||||
}
|
||||
|
||||
function addMeta(source, text, location) {
|
||||
if (options.includeMetadata) {
|
||||
return Object.assign(
|
||||
{},
|
||||
source,
|
||||
{
|
||||
text: text,
|
||||
location: simpleLocation(location),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
function simpleLocation(location) {
|
||||
// Returns an object representing the position of the function within the expression,
|
||||
// demarcated by the position of its first character and last character. We calculate these values
|
||||
// using the offset because the expression could span multiple lines, and we don't want to deal
|
||||
// with column and line values.
|
||||
return {
|
||||
min: location.start.offset,
|
||||
max: location.end.offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start
|
||||
= space? query:OrQuery space? {
|
||||
if (query.type === 'literal') {
|
||||
return addMeta(nodeTypes.function.buildNode('and', [query], 'implicit'), text(), location());
|
||||
}
|
||||
return query;
|
||||
}
|
||||
/ whitespace:[\ \t\r\n]* {
|
||||
return addMeta(nodeTypes.function.buildNode('is', '*', '*'), text(), location());
|
||||
}
|
||||
|
||||
OrQuery
|
||||
= left:AndQuery space 'or'i space right:OrQuery {
|
||||
return addMeta(nodeTypes.function.buildNode('or', [left, right], 'operator'), text(), location());
|
||||
}
|
||||
/ AndQuery
|
||||
|
||||
AndQuery
|
||||
= left:NegatedClause space 'and'i space right:AndQuery {
|
||||
return addMeta(nodeTypes.function.buildNode('and', [left, right], 'operator'), text(), location());
|
||||
}
|
||||
/ left:NegatedClause space !'or'i right:AndQuery {
|
||||
return addMeta(nodeTypes.function.buildNode('and', [left, right], 'implicit'), text(), location());
|
||||
}
|
||||
/ NegatedClause
|
||||
|
||||
NegatedClause
|
||||
= [!] clause:Clause {
|
||||
return addMeta(nodeTypes.function.buildNode('not', clause, 'operator'), text(), location());
|
||||
}
|
||||
/ Clause
|
||||
|
||||
Clause
|
||||
= '(' subQuery:start ')' {
|
||||
return subQuery;
|
||||
}
|
||||
/ Term
|
||||
|
||||
Term
|
||||
= field:literal_arg_type ':' value:literal_arg_type {
|
||||
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('is', [field, value], 'operator'), text(), location());
|
||||
}
|
||||
/ field:literal_arg_type ':[' space? gt:literal_arg_type space 'to'i space lt:literal_arg_type space? ']' {
|
||||
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('range', [field, gt, lt], 'operator'), text(), location());
|
||||
}
|
||||
/ function
|
||||
/ !Keywords literal:literal_arg_type { return literal; }
|
||||
|
||||
function_name
|
||||
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* { return first.join('') + rest.join('') }
|
||||
|
||||
function "function"
|
||||
= name:function_name space? '(' space? arg_list:arg_list? space? ')' {
|
||||
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes(name, arg_list || [], 'function'), text(), location());
|
||||
}
|
||||
|
||||
arg_list
|
||||
= first:argument rest:(space? ',' space? arg:argument {return arg})* space? ','? {
|
||||
return [first].concat(rest);
|
||||
}
|
||||
|
||||
argument
|
||||
= name:function_name space? '=' space? value:arg_type {
|
||||
return addMeta(nodeTypes.namedArg.buildNode(name, value), text(), location());
|
||||
}
|
||||
/ element:arg_type {return element}
|
||||
|
||||
arg_type
|
||||
= OrQuery
|
||||
/ literal_arg_type
|
||||
|
||||
literal_arg_type
|
||||
= literal:literal {
|
||||
var result = addMeta(nodeTypes.literal.buildNode(literal), text(), location());
|
||||
return result;
|
||||
}
|
||||
|
||||
Keywords
|
||||
= 'and'i / 'or'i
|
||||
|
||||
/* ----- Core types ----- */
|
||||
|
||||
literal "literal"
|
||||
= '"' chars:dq_char* '"' { return chars.join(''); } // double quoted string
|
||||
/ "'" chars:sq_char* "'" { return chars.join(''); } // single quoted string
|
||||
/ 'true' { return true; } // unquoted literals from here down
|
||||
/ 'false' { return false; }
|
||||
/ 'null' { return null; }
|
||||
/ string:[^\[\]()"',:=\ \t]+ { // this also matches numbers via Number()
|
||||
var result = string.join('');
|
||||
// Sort of hacky, but PEG doesn't have backtracking so
|
||||
// a number rule is hard to read, and performs worse
|
||||
if (isNaN(Number(result))) return result;
|
||||
return Number(result)
|
||||
}
|
||||
|
||||
space
|
||||
= [\ \t\r\n]+
|
||||
|
||||
dq_char
|
||||
= "\\" sequence:('"' / "\\") { return sequence; }
|
||||
/ [^"] // everything except "
|
||||
|
||||
sq_char
|
||||
= "\\" sequence:("'" / "\\") { return sequence; }
|
||||
/ [^'] // everything except '
|
||||
|
||||
integer
|
||||
= digits:[0-9]+ {return parseInt(digits.join(''))}
|
36
src/ui/public/kuery/filter_migration/__tests__/exists.js
Normal file
36
src/ui/public/kuery/filter_migration/__tests__/exists.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import expect from 'expect.js';
|
||||
import { convertExistsFilter } from '../exists';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('exists filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'exists',
|
||||
key: 'foo',
|
||||
}
|
||||
};
|
||||
const result = convertExistsFilter(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'exists');
|
||||
expect(result.arguments[0].value).to.be('foo');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "exists"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertExistsFilter).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "exists", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import { filterToKueryAST } from '../filter_to_kuery';
|
||||
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('filterToKueryAST', function () {
|
||||
|
||||
it('should hand off conversion of known filter types to the appropriate converter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'exists',
|
||||
key: 'foo',
|
||||
}
|
||||
};
|
||||
const result = filterToKueryAST(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'exists');
|
||||
});
|
||||
|
||||
it('should thrown an error when an unknown filter type is encountered', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo',
|
||||
}
|
||||
};
|
||||
|
||||
expect(filterToKueryAST).withArgs(filter).to.throwException(/Couldn't convert that filter to a kuery/);
|
||||
});
|
||||
|
||||
it('should wrap the AST node of negated filters in a "not" function', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'exists',
|
||||
key: 'foo',
|
||||
}
|
||||
};
|
||||
const negatedFilter = _.set(_.cloneDeep(filter), 'meta.negate', true);
|
||||
|
||||
const result = filterToKueryAST(filter);
|
||||
const negatedResult = filterToKueryAST(negatedFilter);
|
||||
|
||||
expect(negatedResult).to.have.property('type', 'function');
|
||||
expect(negatedResult).to.have.property('function', 'not');
|
||||
expectDeepEqual(negatedResult.arguments[0], result);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import { convertGeoBoundingBox } from '../geo_bounding_box';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('geo_bounding_box filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'geo_bounding_box',
|
||||
key: 'foo',
|
||||
params: {
|
||||
topLeft: {
|
||||
lat: 10,
|
||||
lon: 20,
|
||||
},
|
||||
bottomRight: {
|
||||
lat: 30,
|
||||
lon: 40,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
const result = convertGeoBoundingBox(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'geoBoundingBox');
|
||||
|
||||
const { arguments: [ { value: fieldName }, ...args ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
|
||||
const argByName = _.mapKeys(args, 'name');
|
||||
expect(argByName.topLeft.value.value).to.be('10, 20');
|
||||
expect(argByName.bottomRight.value.value).to.be('30, 40');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "geo_bounding_box"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertGeoBoundingBox).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "geo_bounding_box", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import expect from 'expect.js';
|
||||
import { convertGeoPolygon } from '../geo_polygon';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('geo_polygon filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'geo_polygon',
|
||||
key: 'foo',
|
||||
params: {
|
||||
points: [
|
||||
{
|
||||
lat: 10,
|
||||
lon: 20,
|
||||
},
|
||||
{
|
||||
lat: 30,
|
||||
lon: 40,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
const result = convertGeoPolygon(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'geoPolygon');
|
||||
|
||||
const { arguments: [ { value: fieldName }, ...args ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
|
||||
expect(args[0].value).to.be('10, 20');
|
||||
expect(args[1].value).to.be('30, 40');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "geo_polygon"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertGeoPolygon).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "geo_polygon", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
42
src/ui/public/kuery/filter_migration/__tests__/phrase.js
Normal file
42
src/ui/public/kuery/filter_migration/__tests__/phrase.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import expect from 'expect.js';
|
||||
import { convertPhraseFilter } from '../phrase';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('phrase filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'phrase',
|
||||
key: 'foo',
|
||||
params: {
|
||||
query: 'bar'
|
||||
},
|
||||
}
|
||||
};
|
||||
const result = convertPhraseFilter(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'is');
|
||||
|
||||
const { arguments: [ { value: fieldName }, { value: value } ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
expect(value).to.be('bar');
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "phrase"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertPhraseFilter).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "phrase", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
47
src/ui/public/kuery/filter_migration/__tests__/range.js
Normal file
47
src/ui/public/kuery/filter_migration/__tests__/range.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import { convertRangeFilter } from '../range';
|
||||
|
||||
describe('filter to kuery migration', function () {
|
||||
|
||||
describe('range filter', function () {
|
||||
|
||||
it('should return a kuery node equivalent to the given filter', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'range',
|
||||
key: 'foo',
|
||||
params: {
|
||||
gt: 1000,
|
||||
lt: 8000,
|
||||
},
|
||||
}
|
||||
};
|
||||
const result = convertRangeFilter(filter);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'range');
|
||||
|
||||
const { arguments: [ { value: fieldName }, ...args ] } = result;
|
||||
expect(fieldName).to.be('foo');
|
||||
|
||||
const argByName = _.mapKeys(args, 'name');
|
||||
expect(argByName.gt.value.value).to.be(1000);
|
||||
expect(argByName.lt.value.value).to.be(8000);
|
||||
});
|
||||
|
||||
it('should throw an exception if the given filter is not of type "range"', function () {
|
||||
const filter = {
|
||||
meta: {
|
||||
type: 'foo'
|
||||
}
|
||||
};
|
||||
|
||||
expect(convertRangeFilter).withArgs(filter).to.throwException(
|
||||
/Expected filter of type "range", got "foo"/
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
10
src/ui/public/kuery/filter_migration/exists.js
Normal file
10
src/ui/public/kuery/filter_migration/exists.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function convertExistsFilter(filter) {
|
||||
if (filter.meta.type !== 'exists') {
|
||||
throw new Error(`Expected filter of type "exists", got "${filter.meta.type}"`);
|
||||
}
|
||||
|
||||
const { key } = filter.meta;
|
||||
return nodeTypes.function.buildNode('exists', key);
|
||||
}
|
35
src/ui/public/kuery/filter_migration/filter_to_kuery.js
Normal file
35
src/ui/public/kuery/filter_migration/filter_to_kuery.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { nodeTypes } from '../node_types';
|
||||
import { convertPhraseFilter } from './phrase';
|
||||
import { convertRangeFilter } from './range';
|
||||
import { convertExistsFilter } from './exists';
|
||||
import { convertGeoBoundingBox } from './geo_bounding_box';
|
||||
import { convertGeoPolygon } from './geo_polygon';
|
||||
|
||||
const conversionChain = [
|
||||
convertPhraseFilter,
|
||||
convertRangeFilter,
|
||||
convertExistsFilter,
|
||||
convertGeoBoundingBox,
|
||||
convertGeoPolygon,
|
||||
];
|
||||
|
||||
export function filterToKueryAST(filter) {
|
||||
const { negate } = filter.meta;
|
||||
|
||||
const node = conversionChain.reduce((acc, converter) => {
|
||||
if (acc !== null) return acc;
|
||||
|
||||
try {
|
||||
return converter(filter);
|
||||
}
|
||||
catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}, null);
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Couldn't convert that filter to a kuery`);
|
||||
}
|
||||
|
||||
return negate ? nodeTypes.function.buildNode('not', node) : node;
|
||||
}
|
12
src/ui/public/kuery/filter_migration/geo_bounding_box.js
Normal file
12
src/ui/public/kuery/filter_migration/geo_bounding_box.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function convertGeoBoundingBox(filter) {
|
||||
if (filter.meta.type !== 'geo_bounding_box') {
|
||||
throw new Error(`Expected filter of type "geo_bounding_box", got "${filter.meta.type}"`);
|
||||
}
|
||||
|
||||
const { key, params } = filter.meta;
|
||||
const camelParams = _.mapKeys(params, (value, key) => _.camelCase(key));
|
||||
return nodeTypes.function.buildNode('geoBoundingBox', key, camelParams);
|
||||
}
|
10
src/ui/public/kuery/filter_migration/geo_polygon.js
Normal file
10
src/ui/public/kuery/filter_migration/geo_polygon.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function convertGeoPolygon(filter) {
|
||||
if (filter.meta.type !== 'geo_polygon') {
|
||||
throw new Error(`Expected filter of type "geo_polygon", got "${filter.meta.type}"`);
|
||||
}
|
||||
|
||||
const { key, params: { points } } = filter.meta;
|
||||
return nodeTypes.function.buildNode('geoPolygon', key, points);
|
||||
}
|
1
src/ui/public/kuery/filter_migration/index.js
Normal file
1
src/ui/public/kuery/filter_migration/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { filterToKueryAST } from './filter_to_kuery';
|
10
src/ui/public/kuery/filter_migration/phrase.js
Normal file
10
src/ui/public/kuery/filter_migration/phrase.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function convertPhraseFilter(filter) {
|
||||
if (filter.meta.type !== 'phrase') {
|
||||
throw new Error(`Expected filter of type "phrase", got "${filter.meta.type}"`);
|
||||
}
|
||||
|
||||
const { key, params } = filter.meta;
|
||||
return nodeTypes.function.buildNode('is', key, params.query);
|
||||
}
|
10
src/ui/public/kuery/filter_migration/range.js
Normal file
10
src/ui/public/kuery/filter_migration/range.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function convertRangeFilter(filter) {
|
||||
if (filter.meta.type !== 'range') {
|
||||
throw new Error(`Expected filter of type "range", got "${filter.meta.type}"`);
|
||||
}
|
||||
|
||||
const { key, params } = filter.meta;
|
||||
return nodeTypes.function.buildNode('range', key, params);
|
||||
}
|
97
src/ui/public/kuery/functions/__tests__/and.js
Normal file
97
src/ui/public/kuery/functions/__tests__/and.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import expect from 'expect.js';
|
||||
import * as and from '../and';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import * as ast from '../../ast';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
|
||||
const childNode1 = nodeTypes.function.buildNode('is', 'response', 200);
|
||||
const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg');
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('and', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('should return "arguments" and "serializeStyle" params', function () {
|
||||
const result = and.buildNodeParams([childNode1, childNode2]);
|
||||
expect(result).to.only.have.keys('arguments', 'serializeStyle');
|
||||
});
|
||||
|
||||
it('arguments should contain the unmodified child nodes', function () {
|
||||
const result = and.buildNodeParams([childNode1, childNode2]);
|
||||
const { arguments: [ actualChildNode1, actualChildNode2 ] } = result;
|
||||
expect(actualChildNode1).to.be(childNode1);
|
||||
expect(actualChildNode2).to.be(childNode2);
|
||||
});
|
||||
|
||||
it('serializeStyle should default to "operator"', function () {
|
||||
const { serializeStyle } = and.buildNodeParams([childNode1, childNode2]);
|
||||
expect(serializeStyle).to.be('operator');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should wrap subqueries in an ES bool query\'s filter clause', function () {
|
||||
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2]);
|
||||
const result = and.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.only.have.keys('bool');
|
||||
expect(result.bool).to.only.have.keys('filter');
|
||||
expect(result.bool.filter).to.eql(
|
||||
[childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern))
|
||||
);
|
||||
});
|
||||
|
||||
it('should wrap a literal argument with an "is" function targeting all fields', function () {
|
||||
const literalFoo = nodeTypes.literal.buildNode('foo');
|
||||
const node = nodeTypes.function.buildNode('and', [literalFoo]);
|
||||
const result = and.toElasticsearchQuery(node, indexPattern);
|
||||
const resultChild = result.bool.filter[0];
|
||||
expect(resultChild).to.have.property('simple_query_string');
|
||||
expect(resultChild.simple_query_string.all_fields).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should serialize "and" nodes with an implicit syntax when requested', function () {
|
||||
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2], 'implicit');
|
||||
const result = and.toKueryExpression(node);
|
||||
expect(result).to.be('"response":200 "extension":"jpg"');
|
||||
});
|
||||
|
||||
it('should serialize "and" nodes with an operator syntax when requested', function () {
|
||||
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2], 'operator');
|
||||
const result = and.toKueryExpression(node);
|
||||
expect(result).to.be('"response":200 and "extension":"jpg"');
|
||||
});
|
||||
|
||||
it('should wrap "or" sub-queries in parenthesis', function () {
|
||||
const orNode = nodeTypes.function.buildNode('or', [childNode1, childNode2], 'operator');
|
||||
const fooBarNode = nodeTypes.function.buildNode('is', 'foo', 'bar');
|
||||
const andNode = nodeTypes.function.buildNode('and', [orNode, fooBarNode], 'implicit');
|
||||
|
||||
const result = and.toKueryExpression(andNode);
|
||||
expect(result).to.be('("response":200 or "extension":"jpg") "foo":"bar"');
|
||||
});
|
||||
|
||||
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
|
||||
const node = nodeTypes.function.buildNode('and', [childNode1, childNode2], 'notValid');
|
||||
expect(and.toKueryExpression)
|
||||
.withArgs(node).to.throwException(/Cannot serialize "and" function as "notValid"/);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
54
src/ui/public/kuery/functions/__tests__/exists.js
Normal file
54
src/ui/public/kuery/functions/__tests__/exists.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import expect from 'expect.js';
|
||||
import * as exists from '../exists';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import _ from 'lodash';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('exists', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('should return a single "arguments" param', function () {
|
||||
const result = exists.buildNodeParams('response');
|
||||
expect(result).to.only.have.key('arguments');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided fieldName as a literal', function () {
|
||||
const { arguments: [ arg ] } = exists.buildNodeParams('response');
|
||||
expect(arg).to.have.property('type', 'literal');
|
||||
expect(arg).to.have.property('value', 'response');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return an ES exists query', function () {
|
||||
const expected = {
|
||||
exists: { field: 'response' }
|
||||
};
|
||||
|
||||
const existsNode = nodeTypes.function.buildNode('exists', 'response');
|
||||
const result = exists.toElasticsearchQuery(existsNode, indexPattern);
|
||||
expect(_.isEqual(expected, result)).to.be(true);
|
||||
});
|
||||
|
||||
it('should throw an error for scripted fields', function () {
|
||||
const existsNode = nodeTypes.function.buildNode('exists', 'script string');
|
||||
expect(exists.toElasticsearchQuery)
|
||||
.withArgs(existsNode, indexPattern).to.throwException(/Exists query does not support scripted fields/);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
83
src/ui/public/kuery/functions/__tests__/geo_bounding_box.js
Normal file
83
src/ui/public/kuery/functions/__tests__/geo_bounding_box.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
import expect from 'expect.js';
|
||||
import * as geoBoundingBox from '../geo_bounding_box';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
const params = {
|
||||
bottomRight: {
|
||||
lat: 50.73,
|
||||
lon: -135.35
|
||||
},
|
||||
topLeft: {
|
||||
lat: 73.12,
|
||||
lon: -174.37
|
||||
}
|
||||
};
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('geoBoundingBox', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('should return an "arguments" param', function () {
|
||||
const result = geoBoundingBox.buildNodeParams('geo', params);
|
||||
expect(result).to.only.have.keys('arguments');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided fieldName as a literal', function () {
|
||||
const result = geoBoundingBox.buildNodeParams('geo', params);
|
||||
const { arguments: [ fieldName ] } = result;
|
||||
|
||||
expect(fieldName).to.have.property('type', 'literal');
|
||||
expect(fieldName).to.have.property('value', 'geo');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided params as named arguments with "lat, lon" string values', function () {
|
||||
const result = geoBoundingBox.buildNodeParams('geo', params);
|
||||
const { arguments: [ , ...args ] } = result;
|
||||
|
||||
args.map((param) => {
|
||||
expect(param).to.have.property('type', 'namedArg');
|
||||
expect(['bottomRight', 'topLeft'].includes(param.name)).to.be(true);
|
||||
expect(param.value.type).to.be('literal');
|
||||
|
||||
const expectedParam = params[param.name];
|
||||
const expectedLatLon = `${expectedParam.lat}, ${expectedParam.lon}`;
|
||||
expect(param.value.value).to.be(expectedLatLon);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return an ES geo_bounding_box query representing the given node', function () {
|
||||
const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params);
|
||||
const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.have.property('geo_bounding_box');
|
||||
expect(result.geo_bounding_box.geo).to.have.property('top_left', '73.12, -174.37');
|
||||
expect(result.geo_bounding_box.geo).to.have.property('bottom_right', '50.73, -135.35');
|
||||
});
|
||||
|
||||
it('should use the ignore_unmapped parameter', function () {
|
||||
const node = nodeTypes.function.buildNode('geoBoundingBox', 'geo', params);
|
||||
const result = geoBoundingBox.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result.geo_bounding_box.ignore_unmapped).to.be(true);
|
||||
});
|
||||
|
||||
it('should throw an error for scripted fields', function () {
|
||||
const node = nodeTypes.function.buildNode('geoBoundingBox', 'script number', params);
|
||||
expect(geoBoundingBox.toElasticsearchQuery)
|
||||
.withArgs(node, indexPattern).to.throwException(/Geo bounding box query does not support scripted fields/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
89
src/ui/public/kuery/functions/__tests__/geo_polygon.js
Normal file
89
src/ui/public/kuery/functions/__tests__/geo_polygon.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import expect from 'expect.js';
|
||||
import * as geoPolygon from '../geo_polygon';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
const points = [
|
||||
{
|
||||
lat: 69.77,
|
||||
lon: -171.56
|
||||
},
|
||||
{
|
||||
lat: 50.06,
|
||||
lon: -169.10
|
||||
},
|
||||
{
|
||||
lat: 69.16,
|
||||
lon: -125.85
|
||||
}
|
||||
];
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('geoPolygon', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('should return an "arguments" param', function () {
|
||||
const result = geoPolygon.buildNodeParams('geo', points);
|
||||
expect(result).to.only.have.keys('arguments');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided fieldName as a literal', function () {
|
||||
const result = geoPolygon.buildNodeParams('geo', points);
|
||||
const { arguments: [ fieldName ] } = result;
|
||||
|
||||
expect(fieldName).to.have.property('type', 'literal');
|
||||
expect(fieldName).to.have.property('value', 'geo');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided points literal "lat, lon" string values', function () {
|
||||
const result = geoPolygon.buildNodeParams('geo', points);
|
||||
const { arguments: [ , ...args ] } = result;
|
||||
|
||||
args.forEach((param, index) => {
|
||||
expect(param).to.have.property('type', 'literal');
|
||||
const expectedPoint = points[index];
|
||||
const expectedLatLon = `${expectedPoint.lat}, ${expectedPoint.lon}`;
|
||||
expect(param.value).to.be(expectedLatLon);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return an ES geo_polygon query representing the given node', function () {
|
||||
const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points);
|
||||
const result = geoPolygon.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.have.property('geo_polygon');
|
||||
expect(result.geo_polygon.geo).to.have.property('points');
|
||||
|
||||
result.geo_polygon.geo.points.forEach((point, index) => {
|
||||
const expectedLatLon = `${points[index].lat}, ${points[index].lon}`;
|
||||
expect(point).to.be(expectedLatLon);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should use the ignore_unmapped parameter', function () {
|
||||
const node = nodeTypes.function.buildNode('geoPolygon', 'geo', points);
|
||||
const result = geoPolygon.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result.geo_polygon.ignore_unmapped).to.be(true);
|
||||
});
|
||||
|
||||
it('should throw an error for scripted fields', function () {
|
||||
const node = nodeTypes.function.buildNode('geoPolygon', 'script number', points);
|
||||
expect(geoPolygon.toElasticsearchQuery)
|
||||
.withArgs(node, indexPattern).to.throwException(/Geo polygon query does not support scripted fields/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
133
src/ui/public/kuery/functions/__tests__/is.js
Normal file
133
src/ui/public/kuery/functions/__tests__/is.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
import expect from 'expect.js';
|
||||
import * as is from '../is';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import _ from 'lodash';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('is', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('fieldName and value should be required arguments', function () {
|
||||
expect(is.buildNodeParams).to.throwException(/fieldName is a required argument/);
|
||||
expect(is.buildNodeParams).withArgs('foo').to.throwException(/value is a required argument/);
|
||||
});
|
||||
|
||||
it('should return "arguments" and "serializeStyle" params', function () {
|
||||
const result = is.buildNodeParams('response', 200);
|
||||
expect(result).to.only.have.keys('arguments', 'serializeStyle');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided fieldName and value as literals', function () {
|
||||
const { arguments: [ fieldName, value ] } = is.buildNodeParams('response', 200);
|
||||
|
||||
expect(fieldName).to.have.property('type', 'literal');
|
||||
expect(fieldName).to.have.property('value', 'response');
|
||||
|
||||
expect(value).to.have.property('type', 'literal');
|
||||
expect(value).to.have.property('value', 200);
|
||||
});
|
||||
|
||||
it('serializeStyle should default to "operator"', function () {
|
||||
const { serializeStyle } = is.buildNodeParams('response', 200);
|
||||
expect(serializeStyle).to.be('operator');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return an ES match_all query when fieldName and value are both "*"', function () {
|
||||
const expected = {
|
||||
match_all: {}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('is', '*', '*');
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(_.isEqual(expected, result)).to.be(true);
|
||||
});
|
||||
|
||||
it('should return an ES simple_query_string query in all fields mode when fieldName is "*"', function () {
|
||||
const expected = {
|
||||
simple_query_string: {
|
||||
query: '"200"',
|
||||
all_fields: true,
|
||||
}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('is', '*', 200);
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(_.isEqual(expected, result)).to.be(true);
|
||||
});
|
||||
|
||||
// See discussion about kuery escaping for background:
|
||||
// https://github.com/elastic/kibana/pull/12624#issuecomment-312650307
|
||||
it('should ensure the simple_query_string query is wrapped in double quotes to force a phrase search', function () {
|
||||
const node = nodeTypes.function.buildNode('is', '*', '+response');
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result.simple_query_string.query).to.be('"+response"');
|
||||
});
|
||||
|
||||
it('already double quoted phrases should not get wrapped a second time', function () {
|
||||
const node = nodeTypes.function.buildNode('is', '*', '"+response"');
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result.simple_query_string.query).to.be('"+response"');
|
||||
});
|
||||
|
||||
it('should return an ES exists query when value is "*"', function () {
|
||||
const expected = {
|
||||
exists: { field: 'response' }
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('is', 'response', '*');
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(_.isEqual(expected, result)).to.be(true);
|
||||
});
|
||||
|
||||
it('should return an ES match_phrase query when a concrete fieldName and value are provided', function () {
|
||||
const expected = {
|
||||
match_phrase: {
|
||||
response: 200
|
||||
}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('is', 'response', 200);
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(_.isEqual(expected, result)).to.be(true);
|
||||
});
|
||||
|
||||
it('should support scripted fields', function () {
|
||||
const node = nodeTypes.function.buildNode('is', 'script string', 'foo');
|
||||
const result = is.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.have.key('script');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should serialize "is" nodes with an operator syntax', function () {
|
||||
const node = nodeTypes.function.buildNode('is', 'response', 200, 'operator');
|
||||
const result = is.toKueryExpression(node);
|
||||
expect(result).to.be('"response":200');
|
||||
});
|
||||
|
||||
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
|
||||
const node = nodeTypes.function.buildNode('is', 'response', 200, 'notValid');
|
||||
expect(is.toKueryExpression)
|
||||
.withArgs(node).to.throwException(/Cannot serialize "is" function as "notValid"/);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
97
src/ui/public/kuery/functions/__tests__/not.js
Normal file
97
src/ui/public/kuery/functions/__tests__/not.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import expect from 'expect.js';
|
||||
import * as not from '../not';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import * as ast from '../../ast';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
|
||||
const childNode = nodeTypes.function.buildNode('is', 'response', 200);
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('not', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('should return "arguments" and "serializeStyle" params', function () {
|
||||
const result = not.buildNodeParams(childNode);
|
||||
expect(result).to.only.have.keys('arguments', 'serializeStyle');
|
||||
});
|
||||
|
||||
it('arguments should contain the unmodified child node', function () {
|
||||
const { arguments: [ actualChild ] } = not.buildNodeParams(childNode);
|
||||
expect(actualChild).to.be(childNode);
|
||||
});
|
||||
|
||||
it('serializeStyle should default to "operator"', function () {
|
||||
const { serializeStyle } = not.buildNodeParams(childNode);
|
||||
expect(serializeStyle).to.be('operator');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should wrap a subquery in an ES bool query\'s must_not clause', function () {
|
||||
const node = nodeTypes.function.buildNode('not', childNode);
|
||||
const result = not.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.only.have.keys('bool');
|
||||
expect(result.bool).to.only.have.keys('must_not');
|
||||
expect(result.bool.must_not).to.eql(ast.toElasticsearchQuery(childNode, indexPattern));
|
||||
});
|
||||
|
||||
it('should wrap a literal argument with an "is" function targeting all fields', function () {
|
||||
const literalFoo = nodeTypes.literal.buildNode('foo');
|
||||
const node = nodeTypes.function.buildNode('not', literalFoo);
|
||||
const result = not.toElasticsearchQuery(node, indexPattern);
|
||||
const resultChild = result.bool.must_not;
|
||||
expect(resultChild).to.have.property('simple_query_string');
|
||||
expect(resultChild.simple_query_string.all_fields).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should serialize "not" nodes with an operator syntax', function () {
|
||||
const node = nodeTypes.function.buildNode('not', childNode, 'operator');
|
||||
const result = not.toKueryExpression(node);
|
||||
expect(result).to.be('!"response":200');
|
||||
});
|
||||
|
||||
it('should wrap "and" and "or" sub-queries in parenthesis', function () {
|
||||
const andNode = nodeTypes.function.buildNode('and', [childNode, childNode], 'operator');
|
||||
const notAndNode = nodeTypes.function.buildNode('not', andNode, 'operator');
|
||||
expect(not.toKueryExpression(notAndNode)).to.be('!("response":200 and "response":200)');
|
||||
|
||||
const orNode = nodeTypes.function.buildNode('or', [childNode, childNode], 'operator');
|
||||
const notOrNode = nodeTypes.function.buildNode('not', orNode, 'operator');
|
||||
expect(not.toKueryExpression(notOrNode)).to.be('!("response":200 or "response":200)');
|
||||
});
|
||||
|
||||
it('should not wrap "and" and "or" sub-queries that use the function syntax', function () {
|
||||
const andNode = nodeTypes.function.buildNode('and', [childNode, childNode], 'function');
|
||||
const notAndNode = nodeTypes.function.buildNode('not', andNode, 'operator');
|
||||
expect(not.toKueryExpression(notAndNode)).to.be('!and("response":200, "response":200)');
|
||||
|
||||
const orNode = nodeTypes.function.buildNode('or', [childNode, childNode], 'function');
|
||||
const notOrNode = nodeTypes.function.buildNode('not', orNode, 'operator');
|
||||
expect(not.toKueryExpression(notOrNode)).to.be('!or("response":200, "response":200)');
|
||||
});
|
||||
|
||||
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
|
||||
const node = nodeTypes.function.buildNode('not', childNode, 'notValid');
|
||||
expect(not.toKueryExpression)
|
||||
.withArgs(node).to.throwException(/Cannot serialize "not" function as "notValid"/);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
88
src/ui/public/kuery/functions/__tests__/or.js
Normal file
88
src/ui/public/kuery/functions/__tests__/or.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import expect from 'expect.js';
|
||||
import * as or from '../or';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import * as ast from '../../ast';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
|
||||
const childNode1 = nodeTypes.function.buildNode('is', 'response', 200);
|
||||
const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg');
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('or', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('should return "arguments" and "serializeStyle" params', function () {
|
||||
const result = or.buildNodeParams([childNode1, childNode2]);
|
||||
expect(result).to.only.have.keys('arguments', 'serializeStyle');
|
||||
});
|
||||
|
||||
it('arguments should contain the unmodified child nodes', function () {
|
||||
const result = or.buildNodeParams([childNode1, childNode2]);
|
||||
const { arguments: [ actualChildNode1, actualChildNode2 ] } = result;
|
||||
expect(actualChildNode1).to.be(childNode1);
|
||||
expect(actualChildNode2).to.be(childNode2);
|
||||
});
|
||||
|
||||
it('serializeStyle should default to "operator"', function () {
|
||||
const { serializeStyle } = or.buildNodeParams([childNode1, childNode2]);
|
||||
expect(serializeStyle).to.be('operator');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should wrap subqueries in an ES bool query\'s should clause', function () {
|
||||
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
|
||||
const result = or.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.only.have.keys('bool');
|
||||
expect(result.bool).to.have.keys('should');
|
||||
expect(result.bool.should).to.eql(
|
||||
[childNode1, childNode2].map((childNode) => ast.toElasticsearchQuery(childNode, indexPattern))
|
||||
);
|
||||
});
|
||||
|
||||
it('should wrap a literal argument with an "is" function targeting all fields', function () {
|
||||
const literalFoo = nodeTypes.literal.buildNode('foo');
|
||||
const node = nodeTypes.function.buildNode('or', [literalFoo]);
|
||||
const result = or.toElasticsearchQuery(node, indexPattern);
|
||||
const resultChild = result.bool.should[0];
|
||||
expect(resultChild).to.have.property('simple_query_string');
|
||||
expect(resultChild.simple_query_string.all_fields).to.be(true);
|
||||
});
|
||||
|
||||
it('should require one of the clauses to match', function () {
|
||||
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
|
||||
const result = or.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result.bool).to.have.property('minimum_should_match', 1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should serialize "or" nodes with an operator syntax', function () {
|
||||
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2]);
|
||||
const result = or.toKueryExpression(node);
|
||||
expect(result).to.be('"response":200 or "extension":"jpg"');
|
||||
});
|
||||
|
||||
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
|
||||
const node = nodeTypes.function.buildNode('or', [childNode1, childNode2], 'notValid');
|
||||
expect(or.toKueryExpression)
|
||||
.withArgs(node).to.throwException(/Cannot serialize "or" function as "notValid"/);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
112
src/ui/public/kuery/functions/__tests__/range.js
Normal file
112
src/ui/public/kuery/functions/__tests__/range.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
import expect from 'expect.js';
|
||||
import * as range from '../range';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
import _ from 'lodash';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
let indexPattern;
|
||||
|
||||
describe('kuery functions', function () {
|
||||
|
||||
describe('range', function () {
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNodeParams', function () {
|
||||
|
||||
it('should return "arguments" and "serializeStyle" params', function () {
|
||||
const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 });
|
||||
expect(result).to.only.have.keys('arguments', 'serializeStyle');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided fieldName as a literal', function () {
|
||||
const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 });
|
||||
const { arguments: [ fieldName ] } = result;
|
||||
|
||||
expect(fieldName).to.have.property('type', 'literal');
|
||||
expect(fieldName).to.have.property('value', 'bytes');
|
||||
});
|
||||
|
||||
it('arguments should contain the provided params as named arguments', function () {
|
||||
const givenParams = { gt: 1000, lt: 8000, format: 'epoch_millis' };
|
||||
const result = range.buildNodeParams('bytes', givenParams);
|
||||
const { arguments: [ , ...params ] } = result;
|
||||
|
||||
expect(params).to.be.an('array');
|
||||
expect(params).to.not.be.empty();
|
||||
|
||||
params.map((param) => {
|
||||
expect(param).to.have.property('type', 'namedArg');
|
||||
expect(['gt', 'lt', 'format'].includes(param.name)).to.be(true);
|
||||
expect(param.value.type).to.be('literal');
|
||||
expect(param.value.value).to.be(givenParams[param.name]);
|
||||
});
|
||||
});
|
||||
|
||||
it('serializeStyle should default to "operator"', function () {
|
||||
const result = range.buildNodeParams('bytes', { gte: 1000, lte: 8000 });
|
||||
const { serializeStyle } = result;
|
||||
expect(serializeStyle).to.be('operator');
|
||||
});
|
||||
|
||||
it('serializeStyle should be "function" if either end of the range is exclusive', function () {
|
||||
const result = range.buildNodeParams('bytes', { gt: 1000, lt: 8000 });
|
||||
const { serializeStyle } = result;
|
||||
expect(serializeStyle).to.be('function');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return an ES range query for the node\'s field and params', function () {
|
||||
const expected = {
|
||||
range: {
|
||||
bytes: {
|
||||
gt: 1000,
|
||||
lt: 8000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 });
|
||||
const result = range.toElasticsearchQuery(node, indexPattern);
|
||||
expect(_.isEqual(expected, result)).to.be(true);
|
||||
});
|
||||
|
||||
it('should support scripted fields', function () {
|
||||
const node = nodeTypes.function.buildNode('range', 'script number', { gt: 1000, lt: 8000 });
|
||||
const result = range.toElasticsearchQuery(node, indexPattern);
|
||||
expect(result).to.have.key('script');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should serialize "range" nodes with an operator syntax', function () {
|
||||
const node = nodeTypes.function.buildNode('range', 'bytes', { gte: 1000, lte: 8000 }, 'operator');
|
||||
const result = range.toKueryExpression(node);
|
||||
expect(result).to.be('"bytes":[1000 to 8000]');
|
||||
});
|
||||
|
||||
it('should throw an error for nodes with unknown or undefined serialize styles', function () {
|
||||
const node = nodeTypes.function.buildNode('range', 'bytes', { gte: 1000, lte: 8000 }, 'notValid');
|
||||
expect(range.toKueryExpression)
|
||||
.withArgs(node).to.throwException(/Cannot serialize "range" function as "notValid"/);
|
||||
});
|
||||
|
||||
it('should not support exclusive ranges in the operator syntax', function () {
|
||||
const node = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 });
|
||||
node.serializeStyle = 'operator';
|
||||
expect(range.toKueryExpression)
|
||||
.withArgs(node).to.throwException(/Operator syntax only supports inclusive ranges/);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
46
src/ui/public/kuery/functions/and.js
Normal file
46
src/ui/public/kuery/functions/and.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as ast from '../ast';
|
||||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function buildNodeParams(children, serializeStyle = 'operator') {
|
||||
return {
|
||||
arguments: children,
|
||||
serializeStyle
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const children = node.arguments || [];
|
||||
|
||||
return {
|
||||
bool: {
|
||||
filter: children.map((child) => {
|
||||
if (child.type === 'literal') {
|
||||
child = nodeTypes.function.buildNode('is', '*', child.value);
|
||||
}
|
||||
|
||||
return ast.toElasticsearchQuery(child, indexPattern);
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
if (!['operator', 'implicit'].includes(node.serializeStyle)) {
|
||||
throw new Error(`Cannot serialize "and" function as "${node.serializeStyle}"`);
|
||||
}
|
||||
|
||||
const queryStrings = (node.arguments || []).map((arg) => {
|
||||
const query = ast.toKueryExpression(arg);
|
||||
if (arg.type === 'function' && arg.function === 'or') {
|
||||
return `(${query})`;
|
||||
}
|
||||
return query;
|
||||
});
|
||||
|
||||
if (node.serializeStyle === 'implicit') {
|
||||
return queryStrings.join(' ');
|
||||
}
|
||||
if (node.serializeStyle === 'operator') {
|
||||
return queryStrings.join(' and ');
|
||||
}
|
||||
}
|
20
src/ui/public/kuery/functions/exists.js
Normal file
20
src/ui/public/kuery/functions/exists.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import * as literal from '../node_types/literal';
|
||||
|
||||
export function buildNodeParams(fieldName) {
|
||||
return {
|
||||
arguments: [literal.buildNode(fieldName)],
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const { arguments: [ fieldNameArg ] } = node;
|
||||
const fieldName = literal.toElasticsearchQuery(fieldNameArg);
|
||||
const field = indexPattern.fields.byName[fieldName];
|
||||
|
||||
if (field && field.scripted) {
|
||||
throw new Error(`Exists query does not support scripted fields`);
|
||||
}
|
||||
return {
|
||||
exists: { field: fieldName }
|
||||
};
|
||||
}
|
41
src/ui/public/kuery/functions/geo_bounding_box.js
Normal file
41
src/ui/public/kuery/functions/geo_bounding_box.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import _ from 'lodash';
|
||||
import { nodeTypes } from '../node_types';
|
||||
import * as ast from '../ast';
|
||||
|
||||
export function buildNodeParams(fieldName, params) {
|
||||
params = _.pick(params, 'topLeft', 'bottomRight');
|
||||
const fieldNameArg = nodeTypes.literal.buildNode(fieldName);
|
||||
const args = _.map(params, (value, key) => {
|
||||
const latLon = `${value.lat}, ${value.lon}`;
|
||||
return nodeTypes.namedArg.buildNode(key, latLon);
|
||||
});
|
||||
|
||||
return {
|
||||
arguments: [fieldNameArg, ...args],
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const [ fieldNameArg, ...args ] = node.arguments;
|
||||
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
|
||||
const field = indexPattern.fields.byName[fieldName];
|
||||
const queryParams = args.reduce((acc, arg) => {
|
||||
const snakeArgName = _.snakeCase(arg.name);
|
||||
return {
|
||||
...acc,
|
||||
[snakeArgName]: ast.toElasticsearchQuery(arg),
|
||||
};
|
||||
}, {});
|
||||
|
||||
if (field && field.scripted) {
|
||||
throw new Error(`Geo bounding box query does not support scripted fields`);
|
||||
}
|
||||
|
||||
return {
|
||||
geo_bounding_box: {
|
||||
[fieldName]: queryParams,
|
||||
ignore_unmapped: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
34
src/ui/public/kuery/functions/geo_polygon.js
Normal file
34
src/ui/public/kuery/functions/geo_polygon.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { nodeTypes } from '../node_types';
|
||||
import * as ast from '../ast';
|
||||
|
||||
export function buildNodeParams(fieldName, points) {
|
||||
const fieldNameArg = nodeTypes.literal.buildNode(fieldName);
|
||||
const args = points.map((point) => {
|
||||
const latLon = `${point.lat}, ${point.lon}`;
|
||||
return nodeTypes.literal.buildNode(latLon);
|
||||
});
|
||||
|
||||
return {
|
||||
arguments: [fieldNameArg, ...args],
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const [ fieldNameArg, ...points ] = node.arguments;
|
||||
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
|
||||
const field = indexPattern.fields.byName[fieldName];
|
||||
const queryParams = {
|
||||
points: points.map(ast.toElasticsearchQuery)
|
||||
};
|
||||
|
||||
if (field && field.scripted) {
|
||||
throw new Error(`Geo polygon query does not support scripted fields`);
|
||||
}
|
||||
|
||||
return {
|
||||
geo_polygon: {
|
||||
[fieldName]: queryParams,
|
||||
ignore_unmapped: true,
|
||||
},
|
||||
};
|
||||
}
|
19
src/ui/public/kuery/functions/index.js
Normal file
19
src/ui/public/kuery/functions/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as is from './is';
|
||||
import * as and from './and';
|
||||
import * as or from './or';
|
||||
import * as not from './not';
|
||||
import * as range from './range';
|
||||
import * as exists from './exists';
|
||||
import * as geoBoundingBox from './geo_bounding_box';
|
||||
import * as geoPolygon from './geo_polygon';
|
||||
|
||||
export const functions = {
|
||||
is,
|
||||
and,
|
||||
or,
|
||||
not,
|
||||
range,
|
||||
exists,
|
||||
geoBoundingBox,
|
||||
geoPolygon,
|
||||
};
|
75
src/ui/public/kuery/functions/is.js
Normal file
75
src/ui/public/kuery/functions/is.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import _ from 'lodash';
|
||||
import * as literal from '../node_types/literal';
|
||||
import { getPhraseScript } from 'ui/filter_manager/lib/phrase';
|
||||
|
||||
export function buildNodeParams(fieldName, value, serializeStyle = 'operator') {
|
||||
if (_.isUndefined(fieldName)) {
|
||||
throw new Error('fieldName is a required argument');
|
||||
}
|
||||
if (_.isUndefined(value)) {
|
||||
throw new Error('value is a required argument');
|
||||
}
|
||||
|
||||
return {
|
||||
arguments: [literal.buildNode(fieldName), literal.buildNode(value)],
|
||||
serializeStyle
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const { arguments: [ fieldNameArg, valueArg ] } = node;
|
||||
const fieldName = literal.toElasticsearchQuery(fieldNameArg);
|
||||
const field = indexPattern.fields.byName[fieldName];
|
||||
const value = !_.isUndefined(valueArg) ? literal.toElasticsearchQuery(valueArg) : valueArg;
|
||||
|
||||
if (field && field.scripted) {
|
||||
return {
|
||||
script: {
|
||||
...getPhraseScript(field, value)
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (fieldName === '*' && value === '*') {
|
||||
return { match_all: {} };
|
||||
}
|
||||
else if (fieldName === '*' && value !== '*') {
|
||||
const userQuery = String(value);
|
||||
const query = isDoubleQuoted(userQuery) ? userQuery : `"${userQuery}"`;
|
||||
|
||||
return {
|
||||
simple_query_string: {
|
||||
query,
|
||||
all_fields: true
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (fieldName !== '*' && value === '*') {
|
||||
return {
|
||||
exists: { field: fieldName }
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
match_phrase: {
|
||||
[fieldName]: value
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
if (node.serializeStyle !== 'operator') {
|
||||
throw new Error(`Cannot serialize "is" function as "${node.serializeStyle}"`);
|
||||
}
|
||||
|
||||
const { arguments: [ fieldNameArg, valueArg ] } = node;
|
||||
const fieldName = literal.toKueryExpression(fieldNameArg);
|
||||
const value = !_.isUndefined(valueArg) ? literal.toKueryExpression(valueArg) : valueArg;
|
||||
|
||||
return `${fieldName}:${value}`;
|
||||
}
|
||||
|
||||
function isDoubleQuoted(str) {
|
||||
return str.startsWith('"') && str.endsWith('"');
|
||||
}
|
||||
|
42
src/ui/public/kuery/functions/not.js
Normal file
42
src/ui/public/kuery/functions/not.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import * as ast from '../ast';
|
||||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function buildNodeParams(child, serializeStyle = 'operator') {
|
||||
return {
|
||||
arguments: [child],
|
||||
serializeStyle
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
let [ argument ] = node.arguments;
|
||||
if (argument.type === 'literal') {
|
||||
argument = nodeTypes.function.buildNode('is', '*', argument.value);
|
||||
}
|
||||
|
||||
return {
|
||||
bool: {
|
||||
must_not: ast.toElasticsearchQuery(argument, indexPattern)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
if (node.serializeStyle !== 'operator') {
|
||||
throw new Error(`Cannot serialize "not" function as "${node.serializeStyle}"`);
|
||||
}
|
||||
|
||||
const [ argument ] = node.arguments;
|
||||
const queryString = ast.toKueryExpression(argument);
|
||||
|
||||
if (
|
||||
argument.function &&
|
||||
(argument.function === 'and' || argument.function === 'or') &&
|
||||
argument.serializeStyle !== 'function'
|
||||
) {
|
||||
return `!(${queryString})`;
|
||||
}
|
||||
else {
|
||||
return `!${queryString}`;
|
||||
}
|
||||
}
|
39
src/ui/public/kuery/functions/or.js
Normal file
39
src/ui/public/kuery/functions/or.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import * as ast from '../ast';
|
||||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function buildNodeParams(children, serializeStyle = 'operator') {
|
||||
return {
|
||||
arguments: children,
|
||||
serializeStyle,
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const children = node.arguments || [];
|
||||
|
||||
return {
|
||||
bool: {
|
||||
should: children.map((child) => {
|
||||
if (child.type === 'literal') {
|
||||
child = nodeTypes.function.buildNode('is', '*', child.value);
|
||||
}
|
||||
|
||||
return ast.toElasticsearchQuery(child, indexPattern);
|
||||
}),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
if (node.serializeStyle !== 'operator') {
|
||||
throw new Error(`Cannot serialize "or" function as "${node.serializeStyle}"`);
|
||||
}
|
||||
|
||||
const queryStrings = (node.arguments || []).map((arg) => {
|
||||
return ast.toKueryExpression(arg);
|
||||
});
|
||||
|
||||
return queryStrings.join(' or ');
|
||||
}
|
||||
|
78
src/ui/public/kuery/functions/range.js
Normal file
78
src/ui/public/kuery/functions/range.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import _ from 'lodash';
|
||||
import { nodeTypes } from '../node_types';
|
||||
import * as ast from '../ast';
|
||||
import { getRangeScript } from 'ui/filter_manager/lib/range';
|
||||
|
||||
export function buildNodeParams(fieldName, params, serializeStyle = 'operator') {
|
||||
params = _.pick(params, 'gt', 'lt', 'gte', 'lte', 'format');
|
||||
const fieldNameArg = nodeTypes.literal.buildNode(fieldName);
|
||||
const args = _.map(params, (value, key) => {
|
||||
return nodeTypes.namedArg.buildNode(key, value);
|
||||
});
|
||||
|
||||
// we only support inclusive ranges in the operator syntax currently
|
||||
if (_.has(params, 'gt') || _.has(params, 'lt')) {
|
||||
serializeStyle = 'function';
|
||||
}
|
||||
|
||||
return {
|
||||
arguments: [fieldNameArg, ...args],
|
||||
serializeStyle,
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const [ fieldNameArg, ...args ] = node.arguments;
|
||||
const fieldName = nodeTypes.literal.toElasticsearchQuery(fieldNameArg);
|
||||
const field = indexPattern.fields.byName[fieldName];
|
||||
const namedArgs = extractArguments(args);
|
||||
const queryParams = _.mapValues(namedArgs, ast.toElasticsearchQuery);
|
||||
|
||||
if (field && field.scripted) {
|
||||
return {
|
||||
script: {
|
||||
...getRangeScript(field, queryParams)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
range: {
|
||||
[fieldName]: queryParams
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
if (node.serializeStyle !== 'operator') {
|
||||
throw new Error(`Cannot serialize "range" function as "${node.serializeStyle}"`);
|
||||
}
|
||||
const [ fieldNameArg, ...args ] = node.arguments;
|
||||
const fieldName = ast.toKueryExpression(fieldNameArg);
|
||||
const { gte, lte } = extractArguments(args);
|
||||
|
||||
if (_.isUndefined(gte) || _.isUndefined(lte)) {
|
||||
throw new Error(`Operator syntax only supports inclusive ranges`);
|
||||
}
|
||||
|
||||
return `${fieldName}:[${ast.toKueryExpression(gte)} to ${ast.toKueryExpression(lte)}]`;
|
||||
}
|
||||
|
||||
function extractArguments(args) {
|
||||
if ((args.gt && args.gte) || (args.lt && args.lte)) {
|
||||
throw new Error('range ends cannot be both inclusive and exclusive');
|
||||
}
|
||||
|
||||
const unnamedArgOrder = ['gte', 'lte', 'format'];
|
||||
|
||||
return args.reduce((acc, arg, index) => {
|
||||
if (arg.type === 'namedArg') {
|
||||
acc[arg.name] = arg.value;
|
||||
}
|
||||
else {
|
||||
acc[unnamedArgOrder[index]] = arg;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
3
src/ui/public/kuery/index.js
Normal file
3
src/ui/public/kuery/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './ast';
|
||||
export * from './filter_migration';
|
||||
export * from './node_types';
|
89
src/ui/public/kuery/node_types/__tests__/function.js
Normal file
89
src/ui/public/kuery/node_types/__tests__/function.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import * as functionType from '../function';
|
||||
import _ from 'lodash';
|
||||
import expect from 'expect.js';
|
||||
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
|
||||
import * as isFunction from '../../functions/is';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import ngMock from 'ng_mock';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
|
||||
describe('kuery node types', function () {
|
||||
|
||||
describe('function', function () {
|
||||
|
||||
let indexPattern;
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
}));
|
||||
|
||||
describe('buildNode', function () {
|
||||
|
||||
it('should return a node representing the given kuery function', function () {
|
||||
const result = functionType.buildNode('is', 'response', 200);
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'is');
|
||||
expect(result).to.have.property('arguments');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('buildNodeWithArgumentNodes', function () {
|
||||
|
||||
it('should return a function node with the given argument list untouched', function () {
|
||||
const fieldNameLiteral = nodeTypes.literal.buildNode('response');
|
||||
const valueLiteral = nodeTypes.literal.buildNode(200);
|
||||
const argumentNodes = [fieldNameLiteral, valueLiteral];
|
||||
const result = functionType.buildNodeWithArgumentNodes('is', argumentNodes);
|
||||
|
||||
expect(result).to.have.property('type', 'function');
|
||||
expect(result).to.have.property('function', 'is');
|
||||
expect(result).to.have.property('arguments');
|
||||
expect(result.arguments).to.be(argumentNodes);
|
||||
expectDeepEqual(result.arguments, argumentNodes);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return the given function type\'s ES query representation', function () {
|
||||
const node = functionType.buildNode('is', 'response', 200);
|
||||
const expected = isFunction.toElasticsearchQuery(node, indexPattern);
|
||||
const result = functionType.toElasticsearchQuery(node, indexPattern);
|
||||
expect(_.isEqual(expected, result)).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should return the function syntax representation of the given node by default', function () {
|
||||
const node = functionType.buildNode('exists', 'foo');
|
||||
expect(functionType.toKueryExpression(node)).to.be('exists("foo")');
|
||||
});
|
||||
|
||||
it('should return the function syntax representation of the given node if serializeStyle is "function"', function () {
|
||||
const node = functionType.buildNode('exists', 'foo');
|
||||
node.serializeStyle = 'function';
|
||||
expect(functionType.toKueryExpression(node)).to.be('exists("foo")');
|
||||
});
|
||||
|
||||
it('should defer to the function\'s serializer if another serializeStyle is specified', function () {
|
||||
const node = functionType.buildNode('is', 'response', 200);
|
||||
expect(node.serializeStyle).to.be('operator');
|
||||
expect(functionType.toKueryExpression(node)).to.be('"response":200');
|
||||
});
|
||||
|
||||
it('should simply return the node\'s "text" property if one exists', function () {
|
||||
const node = functionType.buildNode('exists', 'foo');
|
||||
node.text = 'bar';
|
||||
expect(functionType.toKueryExpression(node)).to.be('bar');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
44
src/ui/public/kuery/node_types/__tests__/literal.js
Normal file
44
src/ui/public/kuery/node_types/__tests__/literal.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import expect from 'expect.js';
|
||||
import * as literal from '../literal';
|
||||
|
||||
describe('kuery node types', function () {
|
||||
|
||||
describe('literal', function () {
|
||||
|
||||
describe('buildNode', function () {
|
||||
|
||||
it('should return a node representing the given value', function () {
|
||||
const result = literal.buildNode('foo');
|
||||
expect(result).to.have.property('type', 'literal');
|
||||
expect(result).to.have.property('value', 'foo');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return the literal value represented by the given node', function () {
|
||||
const node = literal.buildNode('foo');
|
||||
const result = literal.toElasticsearchQuery(node);
|
||||
expect(result).to.be('foo');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should return the literal value represented by the given node', function () {
|
||||
const numberNode = literal.buildNode(200);
|
||||
expect(literal.toKueryExpression(numberNode)).to.be(200);
|
||||
});
|
||||
|
||||
it('should wrap string values in double quotes', function () {
|
||||
const stringNode = literal.buildNode('foo');
|
||||
expect(literal.toKueryExpression(stringNode)).to.be('"foo"');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
53
src/ui/public/kuery/node_types/__tests__/named_arg.js
Normal file
53
src/ui/public/kuery/node_types/__tests__/named_arg.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import expect from 'expect.js';
|
||||
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
|
||||
import * as namedArg from '../named_arg';
|
||||
import { nodeTypes } from '../../node_types';
|
||||
|
||||
describe('kuery node types', function () {
|
||||
|
||||
describe('named arg', function () {
|
||||
|
||||
describe('buildNode', function () {
|
||||
|
||||
it('should return a node representing a named argument with the given value', function () {
|
||||
const result = namedArg.buildNode('fieldName', 'foo');
|
||||
expect(result).to.have.property('type', 'namedArg');
|
||||
expect(result).to.have.property('name', 'fieldName');
|
||||
expect(result).to.have.property('value');
|
||||
|
||||
const literalValue = result.value;
|
||||
expect(literalValue).to.have.property('type', 'literal');
|
||||
expect(literalValue).to.have.property('value', 'foo');
|
||||
});
|
||||
|
||||
it('should support literal nodes as values', function () {
|
||||
const value = nodeTypes.literal.buildNode('foo');
|
||||
const result = namedArg.buildNode('fieldName', value);
|
||||
expect(result.value).to.be(value);
|
||||
expectDeepEqual(result.value, value);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toElasticsearchQuery', function () {
|
||||
|
||||
it('should return the argument value represented by the given node', function () {
|
||||
const node = namedArg.buildNode('fieldName', 'foo');
|
||||
const result = namedArg.toElasticsearchQuery(node);
|
||||
expect(result).to.be('foo');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('toKueryExpression', function () {
|
||||
|
||||
it('should return the argument name and value represented by the given node', function () {
|
||||
const node = namedArg.buildNode('fieldName', 'foo');
|
||||
expect(namedArg.toKueryExpression(node)).to.be('fieldName="foo"');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
54
src/ui/public/kuery/node_types/function.js
Normal file
54
src/ui/public/kuery/node_types/function.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import _ from 'lodash';
|
||||
import { functions } from '../functions';
|
||||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function buildNode(functionName, ...functionArgs) {
|
||||
const kueryFunction = functions[functionName];
|
||||
|
||||
if (_.isUndefined(kueryFunction)) {
|
||||
throw new Error(`Unknown function "${functionName}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
function: functionName,
|
||||
...kueryFunction.buildNodeParams(...functionArgs)
|
||||
};
|
||||
}
|
||||
|
||||
// Mainly only useful in the grammar where we'll already have real argument nodes in hand
|
||||
export function buildNodeWithArgumentNodes(functionName, argumentNodes, serializeStyle = 'function') {
|
||||
if (_.isUndefined(functions[functionName])) {
|
||||
throw new Error(`Unknown function "${functionName}"`);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
function: functionName,
|
||||
arguments: argumentNodes,
|
||||
serializeStyle
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node, indexPattern) {
|
||||
const kueryFunction = functions[node.function];
|
||||
return kueryFunction.toElasticsearchQuery(node, indexPattern);
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
const kueryFunction = functions[node.function];
|
||||
|
||||
if (!_.isUndefined(node.text)) {
|
||||
return node.text;
|
||||
}
|
||||
|
||||
if (node.serializeStyle && node.serializeStyle !== 'function') {
|
||||
return kueryFunction.toKueryExpression(node);
|
||||
}
|
||||
|
||||
const functionArguments = (node.arguments || []).map((argument) => {
|
||||
return nodeTypes[argument.type].toKueryExpression(argument);
|
||||
});
|
||||
|
||||
return `${node.function}(${functionArguments.join(', ')})`;
|
||||
}
|
10
src/ui/public/kuery/node_types/index.js
Normal file
10
src/ui/public/kuery/node_types/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as functionType from './function';
|
||||
import * as literal from './literal';
|
||||
import * as namedArg from './named_arg';
|
||||
|
||||
export const nodeTypes = {
|
||||
function: functionType,
|
||||
literal,
|
||||
namedArg,
|
||||
};
|
||||
|
21
src/ui/public/kuery/node_types/literal.js
Normal file
21
src/ui/public/kuery/node_types/literal.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
export function buildNode(value) {
|
||||
return {
|
||||
type: 'literal',
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node) {
|
||||
return node.value;
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
if (_.isString(node.value)) {
|
||||
const escapedValue = node.value.replace(/"/g, '\\"');
|
||||
return `"${escapedValue}"`;
|
||||
}
|
||||
|
||||
return node.value;
|
||||
}
|
20
src/ui/public/kuery/node_types/named_arg.js
Normal file
20
src/ui/public/kuery/node_types/named_arg.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import _ from 'lodash';
|
||||
import * as ast from '../ast';
|
||||
import { nodeTypes } from '../node_types';
|
||||
|
||||
export function buildNode(name, value) {
|
||||
const argumentNode = (_.get(value, 'type') === 'literal') ? value : nodeTypes.literal.buildNode(value);
|
||||
return {
|
||||
type: 'namedArg',
|
||||
name,
|
||||
value: argumentNode,
|
||||
};
|
||||
}
|
||||
|
||||
export function toElasticsearchQuery(node) {
|
||||
return ast.toElasticsearchQuery(node.value);
|
||||
}
|
||||
|
||||
export function toKueryExpression(node) {
|
||||
return `${node.name}=${ast.toKueryExpression(node.value)}`;
|
||||
}
|
|
@ -1,42 +1,35 @@
|
|||
import _ from 'lodash';
|
||||
import { DecorateQueryProvider } from 'ui/courier/data_source/_decorate_query';
|
||||
|
||||
export function ParseQueryLibFromUserProvider(es, Private) {
|
||||
const decorateQuery = Private(DecorateQueryProvider);
|
||||
export function ParseQueryLibFromUserProvider() {
|
||||
|
||||
/**
|
||||
* Take text from the user and make it into a query object
|
||||
* @param {text} user's query input
|
||||
* Take userInput from the user and make it into a query object
|
||||
* @param {userInput} user's query input
|
||||
* @returns {object}
|
||||
*/
|
||||
return function (text) {
|
||||
function getQueryStringQuery(text) {
|
||||
return decorateQuery({ query_string: { query: text } });
|
||||
}
|
||||
return function (userInput) {
|
||||
const matchAll = '';
|
||||
|
||||
const matchAll = getQueryStringQuery('*');
|
||||
|
||||
// If we get an empty object, treat it as a *
|
||||
if (_.isObject(text)) {
|
||||
if (Object.keys(text).length) {
|
||||
return text;
|
||||
} else {
|
||||
if (_.isObject(userInput)) {
|
||||
// If we get an empty object, treat it as a *
|
||||
if (!Object.keys(userInput).length) {
|
||||
return matchAll;
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
|
||||
// Nope, not an object.
|
||||
text = (text || '').trim();
|
||||
if (text.length === 0) return matchAll;
|
||||
userInput = (userInput || '').trim();
|
||||
if (userInput.length === 0) return matchAll;
|
||||
|
||||
if (text[0] === '{') {
|
||||
if (userInput[0] === '{') {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
return JSON.parse(userInput);
|
||||
} catch (e) {
|
||||
return getQueryStringQuery(text);
|
||||
return userInput;
|
||||
}
|
||||
} else {
|
||||
return getQueryStringQuery(text);
|
||||
return userInput;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,8 +12,5 @@ export function toUser(text) {
|
|||
if (text.query_string) return toUser(text.query_string.query);
|
||||
return angular.toJson(text);
|
||||
}
|
||||
if (text === '*') {
|
||||
return '';
|
||||
}
|
||||
return '' + text;
|
||||
}
|
||||
|
|
154
src/ui/public/query_bar/directive/__tests__/query_bar.js
Normal file
154
src/ui/public/query_bar/directive/__tests__/query_bar.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
import angular from 'angular';
|
||||
import sinon from 'sinon';
|
||||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import { expectDeepEqual } from '../../../../../test_utils/expect_deep_equal.js';
|
||||
|
||||
let $parentScope;
|
||||
let $elem;
|
||||
|
||||
const markup = `<query-bar query="query" app-name="name" on-submit="submitHandler($query)"></query-bar>`;
|
||||
|
||||
function init(query, name, isSwitchingEnabled = true) {
|
||||
ngMock.module('kibana');
|
||||
|
||||
ngMock.module('kibana', function ($provide) {
|
||||
$provide.service('config', function () {
|
||||
this.get = sinon.stub().withArgs('search:queryLanguage:switcher:enable').returns(isSwitchingEnabled);
|
||||
});
|
||||
});
|
||||
|
||||
ngMock.inject(function ($injector, $controller, $rootScope, $compile) {
|
||||
$parentScope = $rootScope;
|
||||
|
||||
$parentScope.submitHandler = sinon.stub();
|
||||
$parentScope.name = name;
|
||||
$parentScope.query = query;
|
||||
$elem = angular.element(markup);
|
||||
|
||||
$compile($elem)($parentScope);
|
||||
$elem.scope().$digest();
|
||||
});
|
||||
}
|
||||
|
||||
describe('queryBar directive', function () {
|
||||
|
||||
describe('language selector', function () {
|
||||
|
||||
it('should display a language selector if switching is enabled', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const selectElement = $elem.find('.kuiLocalSearchSelect');
|
||||
expect(selectElement.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should not display a language selector if switching is disabled', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', false);
|
||||
const selectElement = $elem.find('.kuiLocalSearchSelect');
|
||||
expect(selectElement.length).to.be(0);
|
||||
});
|
||||
|
||||
it('should reflect the language of the query in the selector', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
let selectedOption = $elem.find('.kuiLocalSearchSelect :selected');
|
||||
let displayLang = selectedOption.text();
|
||||
expect(displayLang).to.be('lucene');
|
||||
|
||||
$parentScope.query = { query: 'foo', language: 'kuery' };
|
||||
$parentScope.$digest();
|
||||
selectedOption = $elem.find('.kuiLocalSearchSelect :selected');
|
||||
displayLang = selectedOption.text();
|
||||
expect(displayLang).to.be('kuery');
|
||||
});
|
||||
|
||||
it('should call the onSubmit callback when a new language is selected', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const kueryOption = $elem.find('.kuiLocalSearchSelect option[label="kuery"]');
|
||||
kueryOption.prop('selected', true).trigger('change');
|
||||
expect($parentScope.submitHandler.calledOnce).to.be(true);
|
||||
});
|
||||
|
||||
it('should reset the query string provided to the callback when a new language is selected', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const kueryOption = $elem.find('.kuiLocalSearchSelect option[label="kuery"]');
|
||||
kueryOption.prop('selected', true).trigger('change');
|
||||
expectDeepEqual($parentScope.submitHandler.getCall(0).args[0], { query: '', language: 'kuery' });
|
||||
});
|
||||
|
||||
it('should not modify the parent scope\'s query when a new language is selected', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const kueryOption = $elem.find('.kuiLocalSearchSelect option[label="kuery"]');
|
||||
kueryOption.prop('selected', true).trigger('change');
|
||||
expectDeepEqual($parentScope.query, { query: 'foo', language: 'lucene' });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('query string input', function () {
|
||||
|
||||
it('should reflect the query passed into the directive', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const queryInput = $elem.find('.kuiLocalSearchInput');
|
||||
expect(queryInput.val()).to.be('foo');
|
||||
});
|
||||
|
||||
it('changes to the input text should not modify the parent scope\'s query', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const queryInput = $elem.find('.kuiLocalSearchInput');
|
||||
queryInput.val('bar').trigger('input');
|
||||
|
||||
expect($elem.isolateScope().queryBar.localQuery.query).to.be('bar');
|
||||
expect($parentScope.query.query).to.be('foo');
|
||||
});
|
||||
|
||||
it('should not call onSubmit until the form is submitted', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const queryInput = $elem.find('.kuiLocalSearchInput');
|
||||
queryInput.val('bar').trigger('input');
|
||||
expect($parentScope.submitHandler.notCalled).to.be(true);
|
||||
|
||||
const submitButton = $elem.find('.kuiLocalSearchButton');
|
||||
submitButton.click();
|
||||
expect($parentScope.submitHandler.called).to.be(true);
|
||||
});
|
||||
|
||||
it('should call onSubmit with the current input text when the form is submitted', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const queryInput = $elem.find('.kuiLocalSearchInput');
|
||||
queryInput.val('bar').trigger('input');
|
||||
const submitButton = $elem.find('.kuiLocalSearchButton');
|
||||
submitButton.click();
|
||||
expectDeepEqual($parentScope.submitHandler.getCall(0).args[0], { query: 'bar', language: 'lucene' });
|
||||
});
|
||||
|
||||
it('should customize the input element for each language', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const luceneInput = $elem.find('.kuiLocalSearchInput');
|
||||
expect(luceneInput.attr('placeholder')).to.be('Search... (e.g. status:200 AND extension:PHP)');
|
||||
|
||||
const helpLink = $elem.find('.kuiLocalSearchAssistedInput__assistance .kuiLink');
|
||||
expect(helpLink.text().trim()).to.be('Uses lucene query syntax');
|
||||
|
||||
$parentScope.query = { query: 'foo', language: 'kuery' };
|
||||
$parentScope.$digest();
|
||||
const kueryInput = $elem.find('.kuiLocalSearchInput');
|
||||
expect(kueryInput.attr('placeholder')).to.be('Search with kuery...');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('typeahead key', function () {
|
||||
|
||||
it('should use a unique typeahead key for each appName/language combo', function () {
|
||||
init({ query: 'foo', language: 'lucene' }, 'discover', true);
|
||||
const typeahead = $elem.find('.typeahead');
|
||||
expect(typeahead.isolateScope().historyKey).to.be('discover-lucene');
|
||||
|
||||
$parentScope.query = { query: 'foo', language: 'kuery' };
|
||||
$parentScope.$digest();
|
||||
expect(typeahead.isolateScope().historyKey).to.be('discover-kuery');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
73
src/ui/public/query_bar/directive/query_bar.html
Normal file
73
src/ui/public/query_bar/directive/query_bar.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
<form
|
||||
role="form"
|
||||
name="queryBarForm"
|
||||
ng-submit="queryBar.submit()"
|
||||
>
|
||||
<div class="typeahead" kbn-typeahead="{{queryBar.typeaheadKey()}}" on-select="queryBar.submit()" role="search">
|
||||
<div class="kuiLocalSearch">
|
||||
|
||||
<!-- Lucene input -->
|
||||
<div class="kuiLocalSearchAssistedInput" ng-if="queryBar.localQuery.language === 'lucene'">
|
||||
<input
|
||||
parse-query
|
||||
input-focus
|
||||
kbn-typeahead-input
|
||||
ng-model="queryBar.localQuery.query"
|
||||
placeholder="Search... (e.g. status:200 AND extension:PHP)"
|
||||
aria-label="Search input"
|
||||
aria-describedby="discover-lucene-syntax-hint"
|
||||
type="text"
|
||||
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
|
||||
ng-class="{'kuiLocalSearchInput-isInvalid': queryBarForm.$invalid}"
|
||||
data-test-subj="queryInput"
|
||||
>
|
||||
<div class="kuiLocalSearchAssistedInput__assistance">
|
||||
<p class="kuiText">
|
||||
<a
|
||||
id="discover-lucene-syntax-hint"
|
||||
class="kuiLink"
|
||||
ng-href="{{queryBar.queryDocLinks.luceneQuerySyntax}}"
|
||||
target="_blank"
|
||||
>
|
||||
Uses lucene query syntax
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- kuery input -->
|
||||
<input
|
||||
ng-if="queryBar.localQuery.language === 'kuery'"
|
||||
ng-model="queryBar.localQuery.query"
|
||||
input-focus
|
||||
kbn-typeahead-input
|
||||
placeholder="Search with kuery..."
|
||||
aria-label="Search input"
|
||||
type="text"
|
||||
class="kuiLocalSearchInput"
|
||||
ng-class="{'kuiLocalSearchInput-isInvalid': queryBarForm.$invalid}"
|
||||
data-test-subj="queryInput"
|
||||
>
|
||||
|
||||
<select
|
||||
class="kuiLocalSearchSelect"
|
||||
ng-options="option for option in queryBar.availableQueryLanguages"
|
||||
ng-model="queryBar.localQuery.language"
|
||||
ng-change="queryBar.selectLanguage()"
|
||||
ng-if="queryBar.showLanguageSwitcher"
|
||||
>
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
aria-label="Search"
|
||||
class="kuiLocalSearchButton"
|
||||
ng-disabled="queryBarForm.$invalid"
|
||||
data-test-subj="querySubmitButton"
|
||||
>
|
||||
<span class="fa fa-search" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>
|
||||
</form>
|
42
src/ui/public/query_bar/directive/query_bar.js
Normal file
42
src/ui/public/query_bar/directive/query_bar.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { uiModules } from 'ui/modules';
|
||||
import template from './query_bar.html';
|
||||
import { queryLanguages } from '../lib/queryLanguages';
|
||||
import { documentationLinks } from '../../documentation_links/documentation_links.js';
|
||||
|
||||
const module = uiModules.get('kibana');
|
||||
|
||||
module.directive('queryBar', function () {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
query: '=',
|
||||
appName: '=?',
|
||||
onSubmit: '&',
|
||||
},
|
||||
controllerAs: 'queryBar',
|
||||
bindToController: true,
|
||||
controller: function ($scope, config) {
|
||||
this.queryDocLinks = documentationLinks.query;
|
||||
this.appName = this.appName || 'global';
|
||||
this.availableQueryLanguages = queryLanguages;
|
||||
this.showLanguageSwitcher = config.get('search:queryLanguage:switcher:enable');
|
||||
this.typeaheadKey = () => `${this.appName}-${this.query.language}`;
|
||||
|
||||
this.submit = () => {
|
||||
this.onSubmit({ $query: this.localQuery });
|
||||
};
|
||||
|
||||
this.selectLanguage = () => {
|
||||
this.localQuery.query = '';
|
||||
this.submit();
|
||||
};
|
||||
|
||||
$scope.$watch('queryBar.query', (newQuery) => {
|
||||
this.localQuery = Object.assign({}, newQuery);
|
||||
}, true);
|
||||
}
|
||||
};
|
||||
|
||||
});
|
1
src/ui/public/query_bar/index.js
Normal file
1
src/ui/public/query_bar/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
import './directive/query_bar';
|
4
src/ui/public/query_bar/lib/queryLanguages.js
Normal file
4
src/ui/public/query_bar/lib/queryLanguages.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const queryLanguages = [
|
||||
'lucene',
|
||||
'kuery',
|
||||
];
|
165
src/ui/public/query_manager/__tests__/query_manager.js
Normal file
165
src/ui/public/query_manager/__tests__/query_manager.js
Normal file
|
@ -0,0 +1,165 @@
|
|||
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
src/ui/public/query_manager/index.js
Normal file
1
src/ui/public/query_manager/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { QueryManagerProvider } from './query_manager';
|
79
src/ui/public/query_manager/query_manager.js
Normal file
79
src/ui/public/query_manager/query_manager.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
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'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
add,
|
||||
addLegacyFilter,
|
||||
};
|
||||
|
||||
};
|
||||
}
|
|
@ -61,24 +61,6 @@ const init = function () {
|
|||
|
||||
describe('typeahead directive', function () {
|
||||
describe('typeahead requirements', function () {
|
||||
describe('missing input', function () {
|
||||
const goodMarkup = markup;
|
||||
|
||||
before(function () {
|
||||
markup = `<div class="typeahead" kbn-typeahead="${typeaheadName}" on-select="selectItem()">
|
||||
<kbn-typeahead-items></kbn-typeahead-items>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
after(function () {
|
||||
markup = goodMarkup;
|
||||
});
|
||||
|
||||
it('should throw with message', function () {
|
||||
expect(init).to.throwException(/kbn-typeahead-input must be defined/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('missing on-select attribute', function () {
|
||||
const goodMarkup = markup;
|
||||
|
||||
|
|
|
@ -23,15 +23,6 @@ typeahead.directive('kbnTypeahead', function () {
|
|||
self.focused = false;
|
||||
self.mousedOver = false;
|
||||
|
||||
// instantiate history and add items to the scope
|
||||
self.history = new PersistedLog('typeahead:' + $scope.historyKey, {
|
||||
maxLength: config.get('history:limit'),
|
||||
filterDuplicates: true
|
||||
});
|
||||
|
||||
$scope.items = self.history.get();
|
||||
$scope.filteredItems = [];
|
||||
|
||||
self.setInputModel = function (model) {
|
||||
$scope.inputModel = model;
|
||||
|
||||
|
@ -194,6 +185,16 @@ typeahead.directive('kbnTypeahead', function () {
|
|||
return !self.hidden && ($scope.filteredItems.length > 0) && (self.focused || self.mousedOver);
|
||||
};
|
||||
|
||||
$scope.$watch('historyKey', () => {
|
||||
self.history = new PersistedLog('typeahead:' + $scope.historyKey, {
|
||||
maxLength: config.get('history:limit'),
|
||||
filterDuplicates: true
|
||||
});
|
||||
|
||||
$scope.items = self.history.get();
|
||||
$scope.filteredItems = [];
|
||||
});
|
||||
|
||||
// handle updates to parent scope history
|
||||
$scope.$watch('items', function () {
|
||||
if (self.query) {
|
||||
|
@ -215,10 +216,6 @@ typeahead.directive('kbnTypeahead', function () {
|
|||
if (!_.has(attrs, 'onSelect')) {
|
||||
throw new Error('on-select must be defined');
|
||||
}
|
||||
// should be defined via setInput() method
|
||||
if (!$scope.inputModel) {
|
||||
throw new Error('kbn-typeahead-input must be defined');
|
||||
}
|
||||
|
||||
$scope.$watch('typeahead.isVisible()', function (vis) {
|
||||
$el.toggleClass('visible', vis);
|
||||
|
|
17
src/ui/public/utils/migrateLegacyQuery.js
Normal file
17
src/ui/public/utils/migrateLegacyQuery.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { has } from 'lodash';
|
||||
|
||||
/**
|
||||
* Creates a standardized query object from old queries that were either strings or pure ES query DSL
|
||||
*
|
||||
* @param query - a legacy query, what used to be stored in SearchSource's query property
|
||||
* @return Object
|
||||
*/
|
||||
export function migrateLegacyQuery(query) {
|
||||
|
||||
// Lucene was the only option before, so language-less queries are all lucene
|
||||
if (!has(query, 'language')) {
|
||||
return { query: query, language: 'lucene' };
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
|
@ -138,7 +138,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
const currentQuery = await PageObjects.dashboard.getQuery();
|
||||
expect(currentQuery).to.equal('');
|
||||
const currentUrl = await remote.getCurrentUrl();
|
||||
const newUrl = currentUrl.replace('query:%27*%27', 'query:%27hi%27');
|
||||
const newUrl = currentUrl.replace('query:%27%27', 'query:%27hi%27');
|
||||
// Don't add the timestamp to the url or it will cause a hard refresh and we want to test a
|
||||
// soft refresh.
|
||||
await remote.get(newUrl.toString(), false);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue