Commit 2da04e72 by Torkel Ödegaard

More progress on influxdb query editor, templating, refactoring, unit tests, #740, #507, #586

parent 141ea7ba
...@@ -11,8 +11,17 @@ function (angular) { ...@@ -11,8 +11,17 @@ function (angular) {
module.controller('InfluxTargetCtrl', function($scope, $timeout) { module.controller('InfluxTargetCtrl', function($scope, $timeout) {
$scope.init = function() { $scope.init = function() {
$scope.target.function = $scope.target.function || 'mean'; var target = $scope.target;
$scope.target.column = $scope.target.column || 'value';
target.function = target.function || 'mean';
target.column = target.column || 'value';
if (target.condition_value) {
target.condition_expression = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value;
delete target.condition_key;
delete target.condition_op;
delete target.condition_value;
}
$scope.rawQuery = false; $scope.rawQuery = false;
...@@ -24,7 +33,7 @@ function (angular) { ...@@ -24,7 +33,7 @@ function (angular) {
]; ];
$scope.operators = ['=', '=~', '>', '<', '!~', '<>']; $scope.operators = ['=', '=~', '>', '<', '!~', '<>'];
$scope.oldSeries = $scope.target.series; $scope.oldSeries = target.series;
$scope.$on('typeahead-updated', function() { $scope.$on('typeahead-updated', function() {
$timeout($scope.get_data); $timeout($scope.get_data);
}); });
......
...@@ -11,7 +11,7 @@ function (angular, app, _, $) { ...@@ -11,7 +11,7 @@ function (angular, app, _, $) {
.module('grafana.directives') .module('grafana.directives')
.directive('templateParamSelector', function($compile) { .directive('templateParamSelector', function($compile) {
var inputTemplate = '<input type="text" data-provide="typeahead" ' + var inputTemplate = '<input type="text" data-provide="typeahead" ' +
' class="grafana-target-segment-input input-medium"' + ' class="grafana-target-text-input input-medium"' +
' spellcheck="false" style="display:none"></input>'; ' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="grafana-target-segment tabindex="1">{{variable.current.text}}</a>'; var buttonTemplate = '<a class="grafana-target-segment tabindex="1">{{variable.current.text}}</a>';
......
<div class="editor-row"> <div class="editor-row">
<div class="section"> <div class="section">
<h5>InfluxDB Query <tip>Example: select text from events where [[timeFilter]]</tip></h5> <h5>InfluxDB Query <tip>Example: select text from events where [[$timeFilter]]</tip></h5>
<div class="editor-option"> <div class="editor-option">
<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where [[timeFilter]]"></input> <input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where [[$timeFilter]]"></input>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -72,6 +72,16 @@ ...@@ -72,6 +72,16 @@
data-min-length=0 data-items=100 data-min-length=0 data-items=100
ng-blur="seriesBlur()"> ng-blur="seriesBlur()">
</li> </li>
<li class="grafana-target-segment">
alias
</li>
<li>
<input type="text" class="input-medium grafana-target-text-input" ng-model="target.alias"
spellcheck='false' placeholder="alias" ng-blur="get_data()">
</li>
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
...@@ -102,16 +112,6 @@ ...@@ -102,16 +112,6 @@
<span influxdb-func-editor class="grafana-target-segment grafana-target-function"> <span influxdb-func-editor class="grafana-target-segment grafana-target-function">
</span> </span>
</li> </li>
<li class="grafana-target-segment">
alias
</li>
<li>
<input type="text" class="input-medium grafana-target-segment-input" ng-model="target.alias"
spellcheck='false' placeholder="alias" ng-blur="get_data()">
</li>
<li> <li>
<a class="grafana-target-segment" ng-click="target.condition_filter = !target.condition_filter; get_data();" <a class="grafana-target-segment" ng-click="target.condition_filter = !target.condition_filter; get_data();"
bs-tooltip="'Add a where clause'" role="menuitem" data-placement="right"> bs-tooltip="'Add a where clause'" role="menuitem" data-placement="right">
...@@ -129,7 +129,7 @@ ...@@ -129,7 +129,7 @@
</li> </li>
<li> <li>
<input type="text" class="input-mini grafana-target-segment-input" ng-model="target.interval" <input type="text" class="input-mini grafana-target-text-input" ng-model="target.interval"
spellcheck='false' placeholder="{{interval}}" data-placement="right" spellcheck='false' placeholder="{{interval}}" data-placement="right"
bs-tooltip="'Leave blank for auto handling based on time range and panel width'" bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
ng-model-onblur ng-change="get_data()" > ng-model-onblur ng-change="get_data()" >
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
</li> </li>
<li ng-show="target.groupby_field_add"> <li ng-show="target.groupby_field_add">
<input type="text" class="input-small grafana-target-segment-input" ng-model="target.groupby_field" <input type="text" class="input-small grafana-target-text-input" ng-model="target.groupby_field"
placeholder="column" spellcheck="false" bs-typeahead="listColumns" data-min-length=0 ng-blur="get_data()"> placeholder="column" spellcheck="false" bs-typeahead="listColumns" data-min-length=0 ng-blur="get_data()">
</li> </li>
......
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
</div> </div>
<div class="editor-option" ng-show="current.includeAll"> <div class="editor-option" ng-show="current.includeAll">
<label class="small">All format</label> <label class="small">All format</label>
<select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex all values', 'comma list', 'custom']" ng-change="typeChanged()"></select> <select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex wildcard', 'regex all values', 'comma list', 'custom']" ng-change="typeChanged()"></select>
</div> </div>
<div class="editor-option" ng-show="current.includeAll"> <div class="editor-option" ng-show="current.includeAll">
<label class="small">All value</label> <label class="small">All value</label>
......
define([
],
function () {
'use strict';
function InfluxQueryBuilder(target) {
this.target = target;
}
var p = InfluxQueryBuilder.prototype;
p.build = function() {
return this.target.rawQuery ? this._modifyRawQuery() : this._buildQuery();
};
p._buildQuery = function() {
var target = this.target;
var query = 'select ';
var seriesName = target.series;
if(!seriesName.match('^/.*/')) {
seriesName = '"' + seriesName+ '"';
}
if (target.groupby_field_add) {
query += target.groupby_field + ', ';
}
query += target.function + '(' + target.column + ')';
query += ' from ' + seriesName + ' where [[$timeFilter]]';
if (target.condition_filter) {
query += ' and ' + target.condition_expression;
}
query += ' group by time([[$interval]])';
if (target.groupby_field_add) {
query += ', ' + target.groupby_field;
this.groupByField = target.groupby_field;
}
query += " order asc";
return query;
};
p._modifyRawQuery = function () {
var query = this.target.query.replace(";", "");
var queryElements = query.split(" ");
var lowerCaseQueryElements = query.toLowerCase().split(" ");
var whereIndex = lowerCaseQueryElements.indexOf("where");
var groupByIndex = lowerCaseQueryElements.indexOf("group");
var orderIndex = lowerCaseQueryElements.indexOf("order");
if (lowerCaseQueryElements[1].indexOf(',') !== -1) {
this.groupByField = lowerCaseQueryElements[1].replace(',', '');
}
if (whereIndex !== -1) {
queryElements.splice(whereIndex + 1, 0, '[[$timeFilter]]', "and");
}
else {
if (groupByIndex !== -1) {
queryElements.splice(groupByIndex, 0, "where", '[[$timeFilter]]');
}
else if (orderIndex !== -1) {
queryElements.splice(orderIndex, 0, "where", '[[$timeFilter]]');
}
else {
queryElements.push("where");
queryElements.push('[[$timeFilter]]');
}
}
return queryElements.join(" ");
};
return InfluxQueryBuilder;
});
...@@ -2,9 +2,10 @@ define([ ...@@ -2,9 +2,10 @@ define([
'angular', 'angular',
'lodash', 'lodash',
'kbn', 'kbn',
'./influxSeries' './influxSeries',
'./influxQueryBuilder'
], ],
function (angular, _, kbn, InfluxSeries) { function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
'use strict'; 'use strict';
var module = angular.module('grafana.services'); var module = angular.module('grafana.services');
...@@ -32,90 +33,25 @@ function (angular, _, kbn, InfluxSeries) { ...@@ -32,90 +33,25 @@ function (angular, _, kbn, InfluxSeries) {
} }
InfluxDatasource.prototype.query = function(options) { InfluxDatasource.prototype.query = function(options) {
var promises = _.map(options.targets, function(target) { var timeFilter = getTimeFilter(options);
var query;
var alias = '';
var promises = _.map(options.targets, function(target) {
if (target.hide || !((target.series && target.column) || target.query)) { if (target.hide || !((target.series && target.column) || target.query)) {
return []; return [];
} }
var timeFilter = getTimeFilter(options); // build query
var groupByField; var queryBuilder = new InfluxQueryBuilder(target);
var query = queryBuilder.build();
if (target.rawQuery) {
query = target.query;
query = query.replace(";", "");
var queryElements = query.split(" ");
var lowerCaseQueryElements = query.toLowerCase().split(" ");
var whereIndex = lowerCaseQueryElements.indexOf("where");
var groupByIndex = lowerCaseQueryElements.indexOf("group");
var orderIndex = lowerCaseQueryElements.indexOf("order");
if (lowerCaseQueryElements[1].indexOf(',') !== -1) {
groupByField = lowerCaseQueryElements[1].replace(',', '');
}
if (whereIndex !== -1) {
queryElements.splice(whereIndex + 1, 0, timeFilter, "and");
}
else {
if (groupByIndex !== -1) {
queryElements.splice(groupByIndex, 0, "where", timeFilter);
}
else if (orderIndex !== -1) {
queryElements.splice(orderIndex, 0, "where", timeFilter);
}
else {
queryElements.push("where");
queryElements.push(timeFilter);
}
}
query = queryElements.join(" ");
query = templateSrv.replace(query);
}
else {
query = 'select ';
var seriesName = target.series;
if(!seriesName.match('^/.*/')) {
seriesName = '"' + seriesName+ '"';
}
if (target.groupby_field_add) {
query += target.groupby_field + ', ';
}
query += target.function + '(' + target.column + ')';
query += ' from ' + seriesName + ' where ' + timeFilter;
if (target.condition_filter) {
query += ' and ' + target.condition_expression;
}
query += ' group by time(' + (target.interval || options.interval) + ')'; // replace templated variables
templateSrv.setGrafanaVariable('$timeFilter', timeFilter);
templateSrv.setGrafanaVariable('$interval', (target.interval || options.interval));
query = templateSrv.replace(query);
if (target.groupby_field_add) { var alias = target.alias ? templateSrv.replace(target.alias) : '';
query += ',' + target.groupby_field;
}
query += " order asc"; var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField);
query = templateSrv.replace(query);
if (target.groupby_field_add) {
groupByField = target.groupby_field;
}
target.query = query;
}
if (target.alias) {
alias = templateSrv.replace(target.alias);
}
var handleResponse = _.partial(handleInfluxQueryResponse, alias, groupByField);
return this._seriesQuery(query).then(handleResponse); return this._seriesQuery(query).then(handleResponse);
}, this); }, this);
...@@ -123,12 +59,11 @@ function (angular, _, kbn, InfluxSeries) { ...@@ -123,12 +59,11 @@ function (angular, _, kbn, InfluxSeries) {
return $q.all(promises).then(function(results) { return $q.all(promises).then(function(results) {
return { data: _.flatten(results) }; return { data: _.flatten(results) };
}); });
}; };
InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
var timeFilter = getTimeFilter({ range: rangeUnparsed }); var timeFilter = getTimeFilter({ range: rangeUnparsed });
var query = _.template(annotation.query, { timeFilter: timeFilter }, this.templateSettings); var query = _.template(annotation.query, { timeFilter: timeFilter, "$timeFilter": timeFilter }, this.templateSettings);
return this._seriesQuery(query).then(function(results) { return this._seriesQuery(query).then(function(results) {
return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations(); return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
......
...@@ -34,6 +34,10 @@ function (angular, _) { ...@@ -34,6 +34,10 @@ function (angular, _) {
this._templateData = _templateData; this._templateData = _templateData;
}; };
this.setGrafanaVariable = function(name, value) {
this._templateData[name] = value;
};
this.replace = function(target) { this.replace = function(target) {
if (!target || target.indexOf('[[') === -1) { if (!target || target.indexOf('[[') === -1) {
return target; return target;
......
...@@ -84,7 +84,7 @@ function (angular, _, kbn) { ...@@ -84,7 +84,7 @@ function (angular, _, kbn) {
this.metricNamesToVariableValues = function(variable, metricNames) { this.metricNamesToVariableValues = function(variable, metricNames) {
var regex, options, i, matches; var regex, options, i, matches;
options = []; options = {}; // use object hash to remove duplicates
if (variable.regex) { if (variable.regex) {
regex = kbn.stringToJsRegex(variable.regex); regex = kbn.stringToJsRegex(variable.regex);
...@@ -101,10 +101,12 @@ function (angular, _, kbn) { ...@@ -101,10 +101,12 @@ function (angular, _, kbn) {
} }
} }
options.push({text: value, value: value}); options[value] = value;
} }
return options; return _.map(_.keys(options), function(key) {
return { text: key, value: key };
});
}; };
this.addAllOption = function(variable) { this.addAllOption = function(variable) {
......
define([ define([
'kbn' 'kbn',
], function(kbn) { 'lodash'
], function(kbn, _) {
'use strict'; 'use strict';
function ControllerTestContext() { function ControllerTestContext() {
...@@ -47,10 +48,17 @@ define([ ...@@ -47,10 +48,17 @@ define([
function ServiceTestContext() { function ServiceTestContext() {
var self = this; var self = this;
self.templateSrv = new TemplateSrvStub();
this.providePhase = function() {
return module(function($provide) {
$provide.value('templateSrv', self.templateSrv);
});
};
this.createService = function(name) { this.createService = function(name) {
return inject([name, '$q', '$rootScope', '$httpBackend', function(InfluxDatasource, $q, $rootScope, $httpBackend) { return inject([name, '$q', '$rootScope', '$httpBackend', function(service, $q, $rootScope, $httpBackend) {
self.service = InfluxDatasource; self.service = service;
self.$q = $q; self.$q = $q;
self.$rootScope = $rootScope; self.$rootScope = $rootScope;
self.$httpBackend = $httpBackend; self.$httpBackend = $httpBackend;
...@@ -82,11 +90,16 @@ define([ ...@@ -82,11 +90,16 @@ define([
function TemplateSrvStub() { function TemplateSrvStub() {
this.variables = []; this.variables = [];
this.replace = function() {}; this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g };
this.data = {};
this.replace = function(text) {
return _.template(text, this.data, this.templateSettings);
};
this.setGrafanaVariable = function(name, value) {
this.data[name] = value;
};
} }
return { return {
ControllerTestContext: ControllerTestContext, ControllerTestContext: ControllerTestContext,
TimeSrvStub: TimeSrvStub, TimeSrvStub: TimeSrvStub,
......
define([
'services/influxdb/influxQueryBuilder'
], function(InfluxQueryBuilder) {
'use strict';
describe('InfluxQueryBuilder', function() {
describe('series with conditon and group by', function() {
var builder = new InfluxQueryBuilder({
series: 'google.test',
column: 'value',
function: 'mean',
condition_filter: true,
condition_expression: "code=1",
groupby_field_add: true,
groupby_field: 'code'
});
var query = builder.build();
it('should generate correct query', function() {
expect(query).to.be('select code, mean(value) from "google.test" where [[$timeFilter]] and code=1 ' +
'group by time([[$interval]]), code order asc');
});
it('should expose groupByFiled', function() {
expect(builder.groupByField).to.be('code');
});
});
describe('old style raw query', function() {
var builder = new InfluxQueryBuilder({
query: 'select host, mean(value) from asd.asd where time > now() - 1h group by time(1s), code order asc',
rawQuery: true
});
var query = builder.build();
it('should generate correct query', function() {
expect(query).to.be('select host, mean(value) from asd.asd where [[$timeFilter]] and time > now() - 1h ' +
' group by time(1s), code order asc');
});
it('should expose groupByFiled', function() {
expect(builder.groupByField).to.be('host');
});
});
});
});
...@@ -8,6 +8,7 @@ define([ ...@@ -8,6 +8,7 @@ define([
var ctx = new helpers.ServiceTestContext(); var ctx = new helpers.ServiceTestContext();
beforeEach(module('grafana.services')); beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createService('InfluxDatasource')); beforeEach(ctx.createService('InfluxDatasource'));
describe('When querying influxdb with one target using query editor target spec', function() { describe('When querying influxdb with one target using query editor target spec', function() {
......
...@@ -140,6 +140,18 @@ define([ ...@@ -140,6 +140,18 @@ define([
}); });
}); });
describeUpdateVariable('regex pattern remove duplicates', function(ctx) {
ctx.setup(function() {
ctx.variable = { type: 'query', query: 'apps.*', name: 'test' };
ctx.variable.regex = 'backend_01';
ctx.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_01.counters.req'}];
});
it('should return matches options', function() {
expect(ctx.variable.options.length).to.be(1);
});
});
describeUpdateVariable('and existing value still exists in options', function(ctx) { describeUpdateVariable('and existing value still exists in options', function(ctx) {
ctx.setup(function() { ctx.setup(function() {
ctx.variable = { type: 'query', query: 'apps.*', name: 'test' }; ctx.variable = { type: 'query', query: 'apps.*', name: 'test' };
...@@ -163,7 +175,29 @@ define([ ...@@ -163,7 +175,29 @@ define([
}); });
}); });
describeUpdateVariable('with include all regex wildcard', function(ctx) { describeUpdateVariable('with include all wildcard', function(ctx) {
ctx.setup(function() {
ctx.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'wildcard' };
ctx.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
it('should add All wildcard option', function() {
expect(ctx.variable.options[0].value).to.be('*');
});
});
describeUpdateVariable('with include all wildcard', function(ctx) {
ctx.setup(function() {
ctx.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'regex wildcard' };
ctx.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
});
it('should add All wildcard option', function() {
expect(ctx.variable.options[0].value).to.be('.*');
});
});
describeUpdateVariable('with include all regex values', function(ctx) {
ctx.setup(function() { ctx.setup(function() {
ctx.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'wildcard' }; ctx.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'wildcard' };
ctx.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}]; ctx.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
......
...@@ -121,6 +121,8 @@ require([ ...@@ -121,6 +121,8 @@ require([
'specs/timeSeries-specs', 'specs/timeSeries-specs',
'specs/row-ctrl-specs', 'specs/row-ctrl-specs',
'specs/graphiteTargetCtrl-specs', 'specs/graphiteTargetCtrl-specs',
'specs/influxSeries-specs',
'specs/influxQueryBuilder-specs',
'specs/influxdb-datasource-specs', 'specs/influxdb-datasource-specs',
'specs/graph-ctrl-specs', 'specs/graph-ctrl-specs',
'specs/grafanaGraph-specs', 'specs/grafanaGraph-specs',
...@@ -130,8 +132,7 @@ require([ ...@@ -130,8 +132,7 @@ require([
'specs/templateValuesSrv-specs', 'specs/templateValuesSrv-specs',
'specs/kbn-format-specs', 'specs/kbn-format-specs',
'specs/dashboardSrv-specs', 'specs/dashboardSrv-specs',
'specs/dashboardViewStateSrv-specs', 'specs/dashboardViewStateSrv-specs'
'specs/influxSeries-specs'
], function () { ], function () {
window.__karma__.start(); window.__karma__.start();
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment