Skip to content

Commit d379e9a

Browse files
authored
Implement new Kibana query language (#12624)
Initial version of an experimental new query language for Kibana.
1 parent 1b02420 commit d379e9a

File tree

102 files changed

+3700
-405
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+3700
-405
lines changed

docs/management/advanced-options.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ compatible with other configuration settings. Deleting a custom setting removes
2222
.Kibana Settings Reference
2323
[horizontal]
2424
`query:queryString:options`:: Options for the Lucene query string parser.
25+
`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.
26+
`search:queryLanguage:switcher:enable`:: Show or hide the query language switcher in the query bar.
2527
`sort:options`:: Options for the Elasticsearch {ref}/search-request-sort.html[sort] parameter.
2628
`dateFormat`:: The format to use for displaying pretty-formatted dates.
2729
`dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser.

src/core_plugins/kibana/public/context/api/__tests__/anchor.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,12 @@ describe('context app', function () {
8888
const setQuerySpy = searchSourceStub.set.withArgs('query');
8989
expect(setQuerySpy.calledOnce).to.be(true);
9090
expect(setQuerySpy.firstCall.args[1]).to.eql({
91-
terms: {
92-
_uid: ['UID'],
91+
query: {
92+
terms: {
93+
_uid: ['UID'],
94+
}
9395
},
96+
language: 'lucene'
9497
});
9598
});
9699
});

src/core_plugins/kibana/public/context/api/anchor.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ function fetchAnchorProvider(courier, Private) {
1515
.set('version', true)
1616
.set('size', 1)
1717
.set('query', {
18-
terms: {
19-
_uid: [uid],
18+
query: {
19+
terms: {
20+
_uid: [uid],
21+
}
2022
},
23+
language: 'lucene'
2124
})
2225
.set('sort', sort);
2326

src/core_plugins/kibana/public/context/api/context.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ function fetchContextProvider(courier, Private) {
5050
.set('size', size)
5151
.set('filter', filters)
5252
.set('query', {
53-
match_all: {},
53+
query: {
54+
match_all: {},
55+
},
56+
language: 'lucene'
5457
})
5558
.set('searchAfter', anchorDocument.sort)
5659
.set('sort', sort);

src/core_plugins/kibana/public/dashboard/dashboard.html

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -31,54 +31,14 @@
3131
</div>
3232

3333
<!-- Search. -->
34-
<form
35-
data-transclude-slot="bottomRow"
36-
class="fullWidth"
37-
ng-show="chrome.getVisible()"
38-
role="form"
39-
name="queryInput"
40-
ng-submit="filterResults()"
41-
>
42-
<div class="typeahead" kbn-typeahead="dashboard" on-select="filterResults()">
43-
<div class="kuiLocalSearch">
44-
<div class="kuiLocalSearchAssistedInput">
45-
<input
46-
parse-query
47-
input-focus
48-
kbn-typeahead-input
49-
ng-model="model.query"
50-
placeholder="Search... (e.g. status:200 AND extension:PHP)"
51-
aria-label="Enter query"
52-
data-test-subj="dashboardQuery"
53-
type="text"
54-
class="kuiLocalSearchInput kuiLocalSearchInput--lucene"
55-
ng-class="{'kuiLocalSearchInput-isInvalid': queryInput.$invalid}"
56-
>
57-
<div class="kuiLocalSearchAssistedInput__assistance">
58-
<p class="kuiText">
59-
<a
60-
class="kuiLink"
61-
ng-href="{{queryDocLinks.luceneQuerySyntax}}"
62-
target="_blank"
63-
>
64-
Uses lucene query syntax
65-
</a>
66-
</p>
67-
</div>
68-
</div>
69-
<button
70-
type="submit"
71-
aria-label="Submit query"
72-
class="kuiLocalSearchButton"
73-
data-test-subj="dashboardQueryFilterButton"
74-
ng-disabled="queryInput.$invalid"
75-
>
76-
<span class="kuiIcon fa-search" aria-hidden="true"></span>
77-
</button>
78-
</div>
79-
<kbn-typeahead-items></kbn-typeahead-items>
80-
</div>
81-
</form>
34+
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
35+
<query-bar
36+
query="model.query"
37+
app-name="'dashboard'"
38+
on-submit="updateQuery($query)"
39+
>
40+
</query-bar>
41+
</div>
8242
</div>
8343
</kbn-top-nav>
8444

@@ -87,6 +47,7 @@
8747
ng-show="showFilterBar()"
8848
state="state"
8949
index-patterns="indexPatterns"
50+
ng-if="model.query.language === 'lucene'"
9051
></filter-bar>
9152

9253
<div
@@ -126,6 +87,7 @@ <h2 class="kuiTitle kuiVerticalRhythm">
12687
toggle-expand="toggleExpandPanel"
12788
register-panel-index-pattern="registerPanelIndexPattern"
12889
data-shared-items-count="{{panels.length}}"
90+
on-filter="filter"
12991
></dashboard-grid>
13092

13193
<dashboard-panel

src/core_plugins/kibana/public/dashboard/dashboard.js

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import chrome from 'ui/chrome';
66

77
import 'plugins/kibana/dashboard/grid';
88
import 'plugins/kibana/dashboard/panel/panel';
9+
import 'ui/query_bar';
910

1011
import { SavedObjectNotFound } from 'ui/errors';
1112
import { getDashboardTitle, getUnsavedChangesWarningMessage } from './dashboard_strings';
@@ -25,6 +26,8 @@ import { notify } from 'ui/notify';
2526
import './panel/get_object_loaders_for_dashboard';
2627
import { documentationLinks } from 'ui/documentation_links/documentation_links';
2728
import { showCloneModal } from './top_nav/show_clone_modal';
29+
import { migrateLegacyQuery } from 'ui/utils/migrateLegacyQuery';
30+
import { QueryManagerProvider } from 'ui/query_manager';
2831
import { ESC_KEY_CODE } from 'ui_framework/services';
2932

3033
const app = uiModules.get('app/dashboard', [
@@ -81,6 +84,7 @@ app.directive('dashboardApp', function ($injector) {
8184
const quickRanges = $injector.get('quickRanges');
8285
const kbnUrl = $injector.get('kbnUrl');
8386
const confirmModal = $injector.get('confirmModal');
87+
const config = $injector.get('config');
8488
const Private = $injector.get('Private');
8589
const brushEvent = Private(UtilsBrushEventProvider);
8690
const filterBarClickHandler = Private(FilterBarClickHandlerProvider);
@@ -100,6 +104,7 @@ app.directive('dashboardApp', function ($injector) {
100104
}
101105

102106
const dashboardState = new DashboardState(dash, AppState, dashboardConfig);
107+
const queryManager = Private(QueryManagerProvider)(dashboardState.getAppState());
103108

104109
// The 'previouslyStored' check is so we only update the time filter on dashboard open, not during
105110
// normal cross app navigation.
@@ -130,7 +135,10 @@ app.directive('dashboardApp', function ($injector) {
130135
updateState();
131136
});
132137

133-
dashboardState.applyFilters(dashboardState.getQuery(), filterBar.getFilters());
138+
dashboardState.applyFilters(
139+
dashboardState.getQuery() || { query: '', language: config.get('search:queryLanguage') },
140+
filterBar.getFilters()
141+
);
134142
let pendingVisCount = _.size(dashboardState.getPanels());
135143

136144
timefilter.enabled = true;
@@ -179,9 +187,14 @@ app.directive('dashboardApp', function ($injector) {
179187
}
180188
};
181189

182-
$scope.filterResults = function () {
183-
dashboardState.applyFilters($scope.model.query, filterBar.getFilters());
184-
$scope.refresh();
190+
$scope.updateQuery = function (query) {
191+
// reset state if language changes
192+
if ($scope.model.query.language && $scope.model.query.language !== query.language) {
193+
filterBar.removeAll();
194+
dashboardState.getAppState().$newFilters = [];
195+
}
196+
197+
$scope.model.query = query;
185198
};
186199

187200
// called by the saved-object-finder when a user clicks a vis
@@ -229,6 +242,28 @@ app.directive('dashboardApp', function ($injector) {
229242
$scope.indexPatterns = dashboardState.getPanelIndexPatterns();
230243
};
231244

245+
$scope.filter = function (field, value, operator, index) {
246+
queryManager.add(field, value, operator, index);
247+
updateState();
248+
};
249+
250+
251+
$scope.$watch('model.query', (newQuery) => {
252+
$scope.model.query = migrateLegacyQuery(newQuery);
253+
dashboardState.applyFilters($scope.model.query, filterBar.getFilters());
254+
$scope.refresh();
255+
});
256+
257+
$scope.$watchCollection(() => dashboardState.getAppState().$newFilters, function (filters = []) {
258+
// need to convert filters generated from user interaction with viz into kuery AST
259+
// These are handled by the filter bar directive when lucene is the query language
260+
Promise.all(filters.map(queryManager.addLegacyFilter))
261+
.then(() => dashboardState.getAppState().$newFilters = [])
262+
.then(updateState)
263+
.then(() => dashboardState.applyFilters($scope.model.query, filterBar.getFilters()))
264+
.then($scope.refresh());
265+
});
266+
232267
$scope.$listen(timefilter, 'fetch', $scope.refresh);
233268

234269
function updateViewMode(newMode) {

src/core_plugins/kibana/public/dashboard/dashboard_context.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22
import _ from 'lodash';
33
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
44
import 'ui/state_management/app_state';
5+
import { luceneStringToDsl } from '../../../../ui/public/courier/data_source/build_query/lucene_string_to_dsl';
56

67
export function dashboardContextProvider(Private, getAppState) {
78
return () => {
89
const queryFilter = Private(FilterBarQueryFilterProvider);
910
const bool = { must: [], must_not: [] };
1011
const filterBarFilters = queryFilter.getFilters();
11-
const queryBarFilter = getAppState().query;
12+
const queryBarQuery = getAppState().query;
1213

13-
// Add the query bar filter, its handled differently.
14-
bool.must.push(queryBarFilter);
14+
if (queryBarQuery.language === 'lucene') {
15+
// Add the query bar filter, its handled differently.
16+
bool.must.push(luceneStringToDsl(queryBarQuery.query));
17+
}
1518

1619
// Add each of the filter bar filters
1720
_.each(filterBarFilters, function (filter) {

src/core_plugins/kibana/public/dashboard/dashboard_state.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,19 @@ export class DashboardState {
219219
* new dashboard, if the query differs from the default.
220220
*/
221221
getQueryChanged() {
222-
return !_.isEqual(this.appState.query, this.getLastSavedQuery());
222+
const currentQuery = this.appState.query;
223+
const lastSavedQuery = this.getLastSavedQuery();
224+
225+
const isLegacyStringQuery = (
226+
_.isString(lastSavedQuery)
227+
&& _.isPlainObject(currentQuery)
228+
&& _.has(currentQuery, 'query')
229+
);
230+
if (isLegacyStringQuery) {
231+
return lastSavedQuery !== currentQuery.query;
232+
}
233+
234+
return !_.isEqual(currentQuery, lastSavedQuery);
223235
}
224236

225237
/**
@@ -404,14 +416,8 @@ export class DashboardState {
404416
*/
405417
applyFilters(query, filters) {
406418
this.appState.query = query;
407-
if (this.appState.query) {
408-
this.savedDashboard.searchSource.set('filter', _.union(filters, [{
409-
query: this.appState.query
410-
}]));
411-
} else {
412-
this.savedDashboard.searchSource.set('filter', filters);
413-
}
414-
419+
this.savedDashboard.searchSource.set('query', query);
420+
this.savedDashboard.searchSource.set('filter', filters);
415421
this.saveState();
416422
}
417423

@@ -424,6 +430,8 @@ export class DashboardState {
424430
this.stateMonitor.ignoreProps('viewMode');
425431
// Filters need to be compared manually because they sometimes have a $$hashkey stored on the object.
426432
this.stateMonitor.ignoreProps('filters');
433+
// Query needs to be compared manually because saved legacy queries get migrated in app state automatically
434+
this.stateMonitor.ignoreProps('query');
427435

428436
this.stateMonitor.onChange(status => {
429437
this.isDirty = status.dirty;

src/core_plugins/kibana/public/dashboard/filter_utils.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@ export class FilterUtils {
3333
* @returns {QueryFilter}
3434
*/
3535
static getQueryFilterForDashboard(dashboard) {
36-
const defaultQueryFilter = { query_string: { query: '*' } };
36+
if (dashboard.searchSource.getOwn('query')) {
37+
return dashboard.searchSource.getOwn('query');
38+
}
39+
3740
const dashboardFilters = this.getDashboardFilters(dashboard);
3841
const dashboardQueryFilter = _.find(dashboardFilters, this.isQueryFilter);
39-
return dashboardQueryFilter ? dashboardQueryFilter.query : defaultQueryFilter;
42+
return dashboardQueryFilter ? dashboardQueryFilter.query : '';
4043
}
4144

4245
/**

src/core_plugins/kibana/public/dashboard/grid.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
6060
* @type {function}
6161
*/
6262
toggleExpand: '=',
63+
/**
64+
* Called when a filter action has been triggered by a panel
65+
* @type {function}
66+
*/
67+
onFilter: '=',
6368
},
6469
link: function ($scope, $el) {
6570
const notify = new Notifier();
@@ -223,7 +228,8 @@ app.directive('dashboardGrid', function ($compile, Notifier) {
223228
app-state="appState"
224229
register-panel-index-pattern="registerPanelIndexPattern"
225230
toggle-expand="toggleExpand(${panel.panelIndex})"
226-
create-child-ui-state="createChildUiState">
231+
create-child-ui-state="createChildUiState"
232+
on-filter="onFilter">
227233
</li>`;
228234
const panelElement = $compile(panelHtml)($scope);
229235
panelElementMapping[panel.panelIndex] = panelElement;

0 commit comments

Comments
 (0)