Skip to content

Commit 83b9db5

Browse files
committed
feat(templating): great progress on adhoc filters, grafana#6038
1 parent 0a44add commit 83b9db5

File tree

11 files changed

+239
-46
lines changed

11 files changed

+239
-46
lines changed

public/app/core/utils/kbn.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ function($, _, moment) {
99
var kbn = {};
1010
kbn.valueFormats = {};
1111

12+
kbn.regexEscape = function(value) {
13+
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
14+
};
15+
1216
///// HELPER FUNCTIONS /////
1317

1418
kbn.round_interval = function(interval) {
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
///<reference path="../../headers/common.d.ts" />
2+
3+
import _ from 'lodash';
4+
import angular from 'angular';
5+
import coreModule from 'app/core/core_module';
6+
7+
export class AdHocFiltersCtrl {
8+
segments: any;
9+
variable: any;
10+
removeTagFilterSegment: any;
11+
12+
/** @ngInject */
13+
constructor(private uiSegmentSrv, private datasourceSrv, private $q, private templateSrv, private $rootScope) {
14+
this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove filter --'});
15+
this.buildSegmentModel();
16+
}
17+
18+
buildSegmentModel() {
19+
this.segments = [];
20+
21+
if (this.variable.value && !_.isArray(this.variable.value)) {
22+
}
23+
24+
for (let tag of this.variable.value) {
25+
if (this.segments.length > 0) {
26+
this.segments.push(this.uiSegmentSrv.newCondition('AND'));
27+
}
28+
29+
if (tag.key !== undefined && tag.value !== undefined) {
30+
this.segments.push(this.uiSegmentSrv.newKey(tag.key));
31+
this.segments.push(this.uiSegmentSrv.newOperator(tag.operator));
32+
this.segments.push(this.uiSegmentSrv.newKeyValue(tag.value));
33+
}
34+
}
35+
36+
this.segments.push(this.uiSegmentSrv.newPlusButton());
37+
}
38+
39+
getOptions(segment, index) {
40+
if (segment.type === 'operator') {
41+
return this.$q.when(this.uiSegmentSrv.newOperators(['=', '!=', '<>', '<', '>', '=~', '!~']));
42+
}
43+
44+
return this.datasourceSrv.get(this.variable.datasource).then(ds => {
45+
var options: any = {};
46+
var promise = null;
47+
48+
if (segment.type !== 'value') {
49+
promise = ds.getTagKeys();
50+
} else {
51+
options.key = this.segments[index-2].value;
52+
promise = ds.getTagValues(options);
53+
}
54+
55+
return promise.then(results => {
56+
results = _.map(results, segment => {
57+
return this.uiSegmentSrv.newSegment({value: segment.text});
58+
});
59+
60+
// add remove option for keys
61+
if (segment.type === 'key') {
62+
results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
63+
}
64+
return results;
65+
});
66+
});
67+
}
68+
69+
segmentChanged(segment, index) {
70+
this.segments[index] = segment;
71+
72+
// handle remove tag condition
73+
if (segment.value === this.removeTagFilterSegment.value) {
74+
this.segments.splice(index, 3);
75+
if (this.segments.length === 0) {
76+
this.segments.push(this.uiSegmentSrv.newPlusButton());
77+
} else if (this.segments.length > 2) {
78+
this.segments.splice(Math.max(index-1, 0), 1);
79+
if (this.segments[this.segments.length-1].type !== 'plus-button') {
80+
this.segments.push(this.uiSegmentSrv.newPlusButton());
81+
}
82+
}
83+
} else {
84+
if (segment.type === 'plus-button') {
85+
if (index > 2) {
86+
this.segments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
87+
}
88+
this.segments.push(this.uiSegmentSrv.newOperator('='));
89+
this.segments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
90+
segment.type = 'key';
91+
segment.cssClass = 'query-segment-key';
92+
}
93+
94+
if ((index+1) === this.segments.length) {
95+
this.segments.push(this.uiSegmentSrv.newPlusButton());
96+
}
97+
}
98+
99+
this.updateVariableModel();
100+
}
101+
102+
updateVariableModel() {
103+
var tags = [];
104+
var tagIndex = -1;
105+
var tagOperator = "";
106+
107+
this.segments.forEach((segment, index) => {
108+
if (segment.fake) {
109+
return;
110+
}
111+
112+
switch (segment.type) {
113+
case 'key': {
114+
tags.push({key: segment.value});
115+
tagIndex += 1;
116+
break;
117+
}
118+
case 'value': {
119+
tags[tagIndex].value = segment.value;
120+
break;
121+
}
122+
case 'operator': {
123+
tags[tagIndex].operator = segment.value;
124+
break;
125+
}
126+
case 'condition': {
127+
break;
128+
}
129+
}
130+
});
131+
132+
this.$rootScope.$broadcast('refresh');
133+
this.variable.value = tags;
134+
}
135+
}
136+
137+
var template = `
138+
<div class="gf-form-inline">
139+
<div class="gf-form" ng-repeat="segment in ctrl.segments">
140+
<metric-segment segment="segment" get-options="ctrl.getOptions(segment, $index)"
141+
on-change="ctrl.segmentChanged(segment, $index)"></metric-segment>
142+
</div>
143+
</div>
144+
`;
145+
146+
export function adHocFiltersComponent() {
147+
return {
148+
restrict: 'E',
149+
template: template,
150+
controller: AdHocFiltersCtrl,
151+
bindToController: true,
152+
controllerAs: 'ctrl',
153+
scope: {
154+
variable: "="
155+
}
156+
};
157+
}
158+
159+
coreModule.directive('adHocFilters', adHocFiltersComponent);

public/app/features/dashboard/all.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ define([
2020
'./import/dash_import',
2121
'./export/export_modal',
2222
'./dash_list_ctrl',
23+
'./ad_hoc_filters',
2324
], function () {});

public/app/features/dashboard/submenu/submenu.html

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
<div class="submenu-controls gf-form-query">
22
<ul ng-if="ctrl.dashboard.templating.list.length > 0">
3-
<li ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item">
3+
<li ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
44
<div class="gf-form">
5-
<label class="gf-form-label template-variable " ng-hide="variable.hide === 1">
5+
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
66
{{variable.label || variable.name}}:
77
</label>
88
<value-select-dropdown ng-if="variable.type !== 'adhoc'" variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>
99
</div>
10-
<span ng-if="variable.type === 'adhoc'">
11-
<div class="gf-form">
12-
<label class="gf-form-label">hostname</label>
13-
<label class="gf-form-label query-operator">=</label>
14-
<label class="gf-form-label">server1</label>
15-
</div>
16-
</span>
10+
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
1711
</li>
1812
</ul>
1913

public/app/features/templating/editorCtrl.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function (angular, _) {
5656
$scope.datasourceTypes = {};
5757
$scope.datasources = _.filter(datasourceSrv.getMetricSources(), function(ds) {
5858
$scope.datasourceTypes[ds.meta.id] = {text: ds.meta.name, value: ds.meta.id};
59-
return !ds.meta.builtIn;
59+
return !ds.meta.builtIn && ds.value !== null;
6060
});
6161

6262
$scope.datasourceTypes = _.map($scope.datasourceTypes, function(value) {

public/app/features/templating/partials/editor.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ <h5 class="section-heading">Query Options</h5>
165165

166166
<div class="gf-form-inline">
167167
<div class="gf-form max-width-21">
168-
<span class="gf-form-label width-7" ng-show="current.type === 'query'">Data source</span>
168+
<span class="gf-form-label width-7">Data source</span>
169169
<div class="gf-form-select-wrapper max-width-14">
170170
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
171171
</div>
@@ -233,6 +233,17 @@ <h5 class="section-heading">Data source options</h5>
233233
</div>
234234
</div>
235235

236+
<div ng-show="current.type === 'adhoc'" class="gf-form-group">
237+
<h5 class="section-heading">Options</h5>
238+
239+
<div class="gf-form max-width-21">
240+
<span class="gf-form-label width-8">Data source</span>
241+
<div class="gf-form-select-wrapper max-width-14">
242+
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
243+
</div>
244+
</div>
245+
</div>
246+
236247
<div class="section gf-form-group" ng-show="showSelectionOptions()">
237248
<h5 class="section-heading">Selection Options</h5>
238249
<div class="section">

public/app/plugins/datasource/influxdb/datasource.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as dateMath from 'app/core/utils/datemath';
77
import InfluxSeries from './influx_series';
88
import InfluxQuery from './influx_query';
99
import ResponseParser from './response_parser';
10+
import InfluxQueryBuilder from './query_builder';
1011

1112
export default class InfluxDatasource {
1213
type: string;
@@ -43,18 +44,35 @@ export default class InfluxDatasource {
4344

4445
query(options) {
4546
var timeFilter = this.getTimeFilter(options);
47+
var scopedVars = _.extend({}, options.scopedVars);
48+
var targets = _.cloneDeep(options.targets);
4649
var queryTargets = [];
4750
var i, y;
4851

49-
var allQueries = _.map(options.targets, (target) => {
52+
var allQueries = _.map(targets, target => {
5053
if (target.hide) { return ""; }
5154

55+
if (!target.rawQuery) {
56+
// apply add hoc filters
57+
for (let variable of this.templateSrv.variables) {
58+
if (variable.type === 'adhoc' && variable.datasource === this.name) {
59+
for (let tag of variable.value) {
60+
if (tag.key !== undefined && tag.value !== undefined) {
61+
target.tags.push({key: tag.key, value: tag.value, condition: 'AND'});
62+
}
63+
}
64+
}
65+
}
66+
}
67+
5268
queryTargets.push(target);
5369

5470
// build query
55-
var queryModel = new InfluxQuery(target, this.templateSrv, options.scopedVars);
71+
scopedVars.interval = {value: target.interval || options.interval};
72+
73+
var queryModel = new InfluxQuery(target, this.templateSrv, scopedVars);
5674
var query = queryModel.render(true);
57-
query = query.replace(/\$interval/g, (target.interval || options.interval));
75+
5876
return query;
5977
}).reduce((acc, current) => {
6078
if (current !== "") {
@@ -64,10 +82,10 @@ export default class InfluxDatasource {
6482
});
6583

6684
// replace grafana variables
67-
allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
85+
scopedVars.timeFilter = {value: timeFilter};
6886

6987
// replace templated variables
70-
allQueries = this.templateSrv.replace(allQueries, options.scopedVars);
88+
allQueries = this.templateSrv.replace(allQueries, scopedVars);
7189

7290
return this._seriesQuery(allQueries).then((data): any => {
7391
if (!data || !data.results) {
@@ -124,24 +142,30 @@ export default class InfluxDatasource {
124142
};
125143

126144
metricFindQuery(query) {
127-
var interpolated;
128-
try {
129-
interpolated = this.templateSrv.replace(query, null, 'regex');
130-
} catch (err) {
131-
return this.$q.reject(err);
132-
}
145+
var interpolated = this.templateSrv.replace(query, null, 'regex');
133146

134147
return this._seriesQuery(interpolated)
135148
.then(_.curry(this.responseParser.parse)(query));
136-
};
149+
}
150+
151+
getTagKeys(options) {
152+
var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
153+
var query = queryBuilder.buildExploreQuery('TAG_KEYS');
154+
return this.metricFindQuery(query);
155+
}
156+
157+
getTagValues(options) {
158+
var queryBuilder = new InfluxQueryBuilder({measurement: '', tags: []}, this.database);
159+
var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
160+
return this.metricFindQuery(query);
161+
}
137162

138163
_seriesQuery(query) {
139164
if (!query) { return this.$q.when({results: []}); }
140165

141166
return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
142167
}
143168

144-
145169
serializeParams(params) {
146170
if (!params) { return '';}
147171

public/app/plugins/datasource/influxdb/influx_query.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import _ from 'lodash';
44
import queryPart from './query_part';
5+
import kbn from 'app/core/utils/kbn';
56

67
export default class InfluxQuery {
78
target: any;
@@ -155,7 +156,7 @@ export default class InfluxQuery {
155156
if (operator !== '>' && operator !== '<') {
156157
value = "'" + value.replace(/\\/g, '\\\\') + "'";
157158
}
158-
} else if (interpolate){
159+
} else if (interpolate) {
159160
value = this.templateSrv.replace(value, this.scopedVars, 'regex');
160161
}
161162

@@ -181,12 +182,26 @@ export default class InfluxQuery {
181182
return policy + measurement;
182183
}
183184

185+
interpolateQueryStr(value, variable, defaultFormatFn) {
186+
// if no multi or include all do not regexEscape
187+
if (!variable.multi && !variable.includeAll) {
188+
return value;
189+
}
190+
191+
if (typeof value === 'string') {
192+
return kbn.regexEscape(value);
193+
}
194+
195+
var escapedValues = _.map(value, kbn.regexEscape);
196+
return escapedValues.join('|');
197+
};
198+
184199
render(interpolate?) {
185200
var target = this.target;
186201

187202
if (target.rawQuery) {
188203
if (interpolate) {
189-
return this.templateSrv.replace(target.query, this.scopedVars, 'regex');
204+
return this.templateSrv.replace(target.query, this.scopedVars, this.interpolateQueryStr);
190205
} else {
191206
return target.query;
192207
}

0 commit comments

Comments
 (0)