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) {
module.controller('InfluxTargetCtrl', function($scope, $timeout) {
$scope.init = function() {
$scope.target.function = $scope.target.function || 'mean';
$scope.target.column = $scope.target.column || 'value';
var target = $scope.target;
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;
......@@ -24,7 +33,7 @@ function (angular) {
];
$scope.operators = ['=', '=~', '>', '<', '!~', '<>'];
$scope.oldSeries = $scope.target.series;
$scope.oldSeries = target.series;
$scope.$on('typeahead-updated', function() {
$timeout($scope.get_data);
});
......
......@@ -11,7 +11,7 @@ function (angular, app, _, $) {
.module('grafana.directives')
.directive('templateParamSelector', function($compile) {
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>';
var buttonTemplate = '<a class="grafana-target-segment tabindex="1">{{variable.current.text}}</a>';
......
<div class="editor-row">
<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">
<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>
......
......@@ -72,6 +72,16 @@
data-min-length=0 data-items=100
ng-blur="seriesBlur()">
</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>
<div class="clearfix"></div>
......@@ -102,16 +112,6 @@
<span influxdb-func-editor class="grafana-target-segment grafana-target-function">
</span>
</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>
<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">
......@@ -129,7 +129,7 @@
</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"
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
ng-model-onblur ng-change="get_data()" >
......@@ -142,7 +142,7 @@
</li>
<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()">
</li>
......
......@@ -101,7 +101,7 @@
</div>
<div class="editor-option" ng-show="current.includeAll">
<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 class="editor-option" ng-show="current.includeAll">
<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([
'angular',
'lodash',
'kbn',
'./influxSeries'
'./influxSeries',
'./influxQueryBuilder'
],
function (angular, _, kbn, InfluxSeries) {
function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) {
'use strict';
var module = angular.module('grafana.services');
......@@ -32,90 +33,25 @@ function (angular, _, kbn, InfluxSeries) {
}
InfluxDatasource.prototype.query = function(options) {
var promises = _.map(options.targets, function(target) {
var query;
var alias = '';
var timeFilter = getTimeFilter(options);
var promises = _.map(options.targets, function(target) {
if (target.hide || !((target.series && target.column) || target.query)) {
return [];
}
var timeFilter = getTimeFilter(options);
var groupByField;
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;
}
// build query
var queryBuilder = new InfluxQueryBuilder(target);
var query = queryBuilder.build();
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) {
query += ',' + target.groupby_field;
}
var alias = target.alias ? templateSrv.replace(target.alias) : '';
query += " order asc";
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);
var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField);
return this._seriesQuery(query).then(handleResponse);
}, this);
......@@ -123,12 +59,11 @@ function (angular, _, kbn, InfluxSeries) {
return $q.all(promises).then(function(results) {
return { data: _.flatten(results) };
});
};
InfluxDatasource.prototype.annotationQuery = function(annotation, 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 new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
......
......@@ -34,6 +34,10 @@ function (angular, _) {
this._templateData = _templateData;
};
this.setGrafanaVariable = function(name, value) {
this._templateData[name] = value;
};
this.replace = function(target) {
if (!target || target.indexOf('[[') === -1) {
return target;
......
......@@ -84,7 +84,7 @@ function (angular, _, kbn) {
this.metricNamesToVariableValues = function(variable, metricNames) {
var regex, options, i, matches;
options = [];
options = {}; // use object hash to remove duplicates
if (variable.regex) {
regex = kbn.stringToJsRegex(variable.regex);
......@@ -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) {
......
define([
'kbn'
], function(kbn) {
'kbn',
'lodash'
], function(kbn, _) {
'use strict';
function ControllerTestContext() {
......@@ -47,10 +48,17 @@ define([
function ServiceTestContext() {
var self = this;
self.templateSrv = new TemplateSrvStub();
this.providePhase = function() {
return module(function($provide) {
$provide.value('templateSrv', self.templateSrv);
});
};
this.createService = function(name) {
return inject([name, '$q', '$rootScope', '$httpBackend', function(InfluxDatasource, $q, $rootScope, $httpBackend) {
self.service = InfluxDatasource;
return inject([name, '$q', '$rootScope', '$httpBackend', function(service, $q, $rootScope, $httpBackend) {
self.service = service;
self.$q = $q;
self.$rootScope = $rootScope;
self.$httpBackend = $httpBackend;
......@@ -82,11 +90,16 @@ define([
function TemplateSrvStub() {
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 {
ControllerTestContext: ControllerTestContext,
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([
var ctx = new helpers.ServiceTestContext();
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createService('InfluxDatasource'));
describe('When querying influxdb with one target using query editor target spec', function() {
......
......@@ -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) {
ctx.setup(function() {
ctx.variable = { type: 'query', query: 'apps.*', name: 'test' };
......@@ -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.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'wildcard' };
ctx.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}];
......
......@@ -121,6 +121,8 @@ require([
'specs/timeSeries-specs',
'specs/row-ctrl-specs',
'specs/graphiteTargetCtrl-specs',
'specs/influxSeries-specs',
'specs/influxQueryBuilder-specs',
'specs/influxdb-datasource-specs',
'specs/graph-ctrl-specs',
'specs/grafanaGraph-specs',
......@@ -130,8 +132,7 @@ require([
'specs/templateValuesSrv-specs',
'specs/kbn-format-specs',
'specs/dashboardSrv-specs',
'specs/dashboardViewStateSrv-specs',
'specs/influxSeries-specs'
'specs/dashboardViewStateSrv-specs'
], function () {
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