Commit 9f60745e by Torkel Ödegaard

Graphite: Graphite query builder can now handle functions that multiple series as arguments! #117

parent 666d6402
......@@ -10,6 +10,7 @@
- [Issue #219](https://github.com/grafana/grafana/issues/219). Templating: Template variable value selection is now a typeahead autocomplete dropdown
**New features and improvements**
- [Issue #117](https://github.com/grafana/grafana/issues/117). Graphite: Graphite query builder can now handle functions that multiple series as arguments!
- [Issue #281](https://github.com/grafana/grafana/issues/281). Graphite: Metric node/segment selection is now a textbox with autocomplete dropdown, allow for custom glob expression for single node segment without entering text editor mode.
- [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
- [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
......
......@@ -9,11 +9,13 @@ function (angular, _, config, gfunc, Parser) {
'use strict';
var module = angular.module('grafana.controllers');
var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
module.controller('GraphiteTargetCtrl', function($scope, $sce, templateSrv) {
$scope.init = function() {
$scope.target.target = $scope.target.target || '';
$scope.targetLetter = targetLetters[$scope.$index];
parseTarget();
};
......@@ -69,6 +71,14 @@ function (angular, _, config, gfunc, Parser) {
$scope.functions.push(innerFunc);
break;
case 'series-ref':
if ($scope.segments.length === 0) {
func.params[index] = astNode.value;
}
else {
func.params[index - 1] = astNode.value;
}
break;
case 'string':
case 'number':
if ((index-1) >= func.def.params.length) {
......@@ -81,9 +91,7 @@ function (angular, _, config, gfunc, Parser) {
else {
func.params[index - 1] = astNode.value;
}
break;
case 'metric':
if ($scope.segments.length > 0) {
throw { message: 'Multiple metric params not supported, use text editor.' };
......@@ -113,8 +121,10 @@ function (angular, _, config, gfunc, Parser) {
return $scope.datasource.metricFindQuery(path)
.then(function(segments) {
if (segments.length === 0) {
$scope.segments = $scope.segments.splice(0, fromIndex);
$scope.segments.push(new MetricSegment('select metric'));
if (path !== '') {
$scope.segments = $scope.segments.splice(0, fromIndex);
$scope.segments.push(new MetricSegment('select metric'));
}
return;
}
if (segments[0].expandable) {
......@@ -144,8 +154,7 @@ function (angular, _, config, gfunc, Parser) {
$scope.getAltSegments = function (index) {
$scope.altSegments = [];
var query = index === 0 ?
'*' : getSegmentPathUpTo(index) + '.*';
var query = index === 0 ? '*' : getSegmentPathUpTo(index) + '.*';
return $scope.datasource.metricFindQuery(query)
.then(function(segments) {
......@@ -226,6 +235,10 @@ function (angular, _, config, gfunc, Parser) {
if (!newFunc.params.length && newFunc.added) {
$scope.targetChanged();
}
if ($scope.segments.length === 1 && $scope.segments[0].value === 'select metric') {
$scope.segments = [];
}
};
$scope.moveAliasFuncLast = function() {
......
......@@ -69,7 +69,6 @@ function (angular, _, $) {
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
......@@ -88,7 +87,6 @@ function (angular, _, $) {
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
}
......@@ -147,7 +145,7 @@ function (angular, _, $) {
$funcLink.appendTo(elem);
_.each(funcDef.params, function(param, index) {
if (param.optional && func.params.length !== index + 1) {
if (param.optional && func.params.length <= index) {
return;
}
......
......@@ -64,10 +64,7 @@ function (angular, app, _, $) {
};
$scope.source = function(query, callback) {
console.log("source!", callback);
if (options) {
return options;
}
if (options) { return options; }
$scope.$apply(function() {
$scope.getAltSegments($scope.$index).then(function() {
......
......@@ -47,6 +47,9 @@
</ul>
<ul class="grafana-target-controls-left">
<li class="grafana-target-segment" style="min-width: 15px; text-align: center">
{{targetLetter}}
</li>
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
......@@ -65,9 +68,7 @@
ng-show="showTextEditor" />
<ul class="grafana-segment-list" role="menu" ng-hide="showTextEditor">
<li ng-repeat="segment in segments" role="menuitem" graphite-segment>
</li>
<li ng-repeat="segment in segments" role="menuitem" graphite-segment></li>
<li ng-repeat="func in functions">
<span graphite-func-editor class="grafana-target-segment grafana-target-function">
</span>
......
......@@ -59,13 +59,23 @@ function (_) {
addFuncDef({
name: 'diffSeries',
params: [
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true }
],
defaultParams: ['$B'],
category: categories.Calculate,
});
addFuncDef({
name: 'asPercent',
params: [{ name: 'other', type: 'value_or_series', optional: true }],
defaultParams: ['$B'],
params: [
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true },
{ name: 'other', type: 'value_or_series', optional: true }
],
defaultParams: ['#A'],
category: categories.Calculate,
});
......@@ -508,7 +518,7 @@ function (_) {
}, this);
if (metricExp !== undefined) {
if (metricExp) {
parameters.unshift(metricExp);
}
......
......@@ -210,31 +210,50 @@ function (angular, _, $, config, kbn, moment) {
return $http(options);
};
GraphiteDatasource.prototype._seriesRefLetters = [
'#A', '#B', '#C', '#D',
'#E', '#F', '#G', '#H',
'#I', '#J', '#K', '#L',
'#M', '#N', '#O'
];
GraphiteDatasource.prototype.buildGraphiteParams = function(options) {
var clean_options = [];
var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
var clean_options = [], targets = {};
var target, targetValue, i;
var regex = /(\#[A-Z])/g;
if (options.format !== 'png') {
options['format'] = 'json';
}
_.each(options, function (value, key) {
if ($.inArray(key, graphite_options) === -1) {
return;
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
targetValue = templateSrv.replace(target.target);
targets[this._seriesRefLetters[i]] = targetValue;
}
if (key === "targets") {
_.each(value, function (value) {
if (value.target && !value.hide) {
var targetValue = templateSrv.replace(value.target);
clean_options.push("target=" + encodeURIComponent(targetValue));
}
}, this);
}
else if (value) {
clean_options.push(key + "=" + encodeURIComponent(value));
function nestedSeriesRegexReplacer(match) {
return targets[match];
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target || target.hide) {
continue;
}
}, this);
targetValue = targets[this._seriesRefLetters[i]];
targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
clean_options.push("target=" + encodeURIComponent(targetValue));
}
_.each(options, function (value, key) {
if ($.inArray(key, graphite_options) === -1) { return; }
clean_options.push(key + "=" + encodeURIComponent(value));
});
return clean_options;
};
......
......@@ -128,6 +128,7 @@ define([
i === 93 || // templateEnd ]
i === 63 || // ?
i === 37 || // %
i === 35 || // #
i >= 97 && i <= 122; // a-z
}
......
......@@ -157,6 +157,7 @@ define([
var param =
this.functionCall() ||
this.numericLiteral() ||
this.seriesRefExpression() ||
this.metricExpression() ||
this.stringLiteral();
......@@ -168,6 +169,24 @@ define([
return [param].concat(this.functionParameters());
},
seriesRefExpression: function() {
if (!this.match('identifier')) {
return null;
}
var value = this.tokens[this.index].value;
if (!value.match(/\#[A-Z]/)) {
return null;
}
var token = this.consumeToken();
return {
type: 'series-ref',
value: token.value
};
},
numericLiteral: function () {
if (!this.match('number')) {
return null;
......
......@@ -211,8 +211,10 @@ input[type=text].grafana-function-param-input {
.grafana-target-controls-left {
list-style: none;
float: left;
width: 30px;
margin: 0px;
li {
display: inline-block;
}
}
.grafana-target-controls {
......
define([
'./helpers',
'services/graphite/graphiteDatasource'
], function(helpers) {
'use strict';
describe('graphiteDatasource', function() {
var ctx = new helpers.ServiceTestContext();
beforeEach(module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createService('GraphiteDatasource'));
beforeEach(function() {
ctx.ds = new ctx.service({ url: [''] });
});
describe('When querying influxdb with one target using query editor target spec', function() {
var query = {
range: { from: 'now-1h', to: 'now' },
targets: [{ target: 'prod1.count' }, {target: 'prod2.count'}],
maxDataPoints: 500
};
var response = [{ target: 'prod1.count', points: [[10, 1], [12,1]], }];
var results;
var request;
beforeEach(function() {
ctx.$httpBackend.expectPOST('/render', function(body) { request = body; return true; })
.respond(response);
ctx.ds.query(query).then(function(data) { results = data; });
ctx.$httpBackend.flush();
});
it('should generate the correct query', function() {
ctx.$httpBackend.verifyNoOutstandingExpectation();
});
it('should query correctly', function() {
var params = request.split('&');
expect(params).to.contain('target=prod1.count');
expect(params).to.contain('target=prod2.count');
expect(params).to.contain('from=-1h');
expect(params).to.contain('until=now');
});
it('should return series list', function() {
expect(results.data.length).to.be(1);
expect(results.data[0].target).to.be('prod1.count');
});
});
describe('building graphite params', function() {
it('should uri escape targets', function() {
var results = ctx.ds.buildGraphiteParams({
targets: [{target: 'prod1.{test,test2}'}, {target: 'prod2.count'}]
});
expect(results).to.contain('target=prod1.%7Btest%2Ctest2%7D');
});
it('should replace target placeholder', function() {
var results = ctx.ds.buildGraphiteParams({
targets: [{target: 'series1'}, {target: 'series2'}, {target: 'asPercent(#A,#B)'}]
});
expect(results[2]).to.be('target=asPercent(series1%2Cseries2)');
});
it('should ignore empty targets', function() {
var results = ctx.ds.buildGraphiteParams({
targets: [{target: 'series1'}, {target: ''}]
});
expect(results.length).to.be(2);
});
});
});
});
......@@ -64,6 +64,59 @@ define([
});
});
describe('when adding function before any metric segment', function() {
beforeEach(function() {
ctx.scope.target.target = '';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}]));
ctx.scope.init();
ctx.scope.$digest();
ctx.scope.$parent = { get_data: sinon.spy() };
ctx.scope.addFunction(gfunc.getFuncDef('asPercent'));
});
it('should add function and remove select metric link', function() {
expect(ctx.scope.segments.length).to.be(0);
});
});
describe('when initalizing target without metric expression and only function', function() {
beforeEach(function() {
ctx.scope.target.target = 'asPercent(#A, #B)';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.scope.init();
ctx.scope.$digest();
ctx.scope.$parent = { get_data: sinon.spy() };
});
it('should not add select metric segment', function() {
expect(ctx.scope.segments.length).to.be(0);
});
it('should add both series refs as params', function() {
expect(ctx.scope.functions[0].params.length).to.be(2);
});
});
describe('when initalizing target without metric expression and function with series-ref', function() {
beforeEach(function() {
ctx.scope.target.target = 'asPercent(metric.node.count, #A)';
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
ctx.scope.init();
ctx.scope.$digest();
ctx.scope.$parent = { get_data: sinon.spy() };
});
it('should add segments', function() {
expect(ctx.scope.segments.length).to.be(3);
});
it('should have correct func params', function() {
expect(ctx.scope.functions[0].params.length).to.be(1);
});
});
describe('targetChanged', function() {
beforeEach(function() {
ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}]));
......
......@@ -156,6 +156,16 @@ define([
expect(rootNode.segments[1].value).to.be('test');
});
it('series parameters', function() {
var parser = new Parser('asPercent(#A, #B)');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('function');
expect(rootNode.params[0].type).to.be('series-ref');
expect(rootNode.params[0].value).to.be('#A');
expect(rootNode.params[1].value).to.be('#B');
});
});
});
......@@ -121,6 +121,7 @@ require([
'specs/timeSeries-specs',
'specs/row-ctrl-specs',
'specs/graphiteTargetCtrl-specs',
'specs/graphiteDatasource-specs',
'specs/influxSeries-specs',
'specs/influxQueryBuilder-specs',
'specs/influxdb-datasource-specs',
......
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