mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Graphite: Graphite query builder can now handle functions that multiple series as arguments! #117
This commit is contained in:
parent
666d640216
commit
9f60745e57
@ -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;
|
||||
}
|
||||
|
||||
function nestedSeriesRegexReplacer(match) {
|
||||
return targets[match];
|
||||
}
|
||||
|
||||
for (i = 0; i < options.targets.length; i++) {
|
||||
target = options.targets[i];
|
||||
if (!target.target || target.hide) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}, 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 {
|
||||
|
84
src/test/specs/graphiteDatasource-specs.js
Normal file
84
src/test/specs/graphiteDatasource-specs.js
Normal file
@ -0,0 +1,84 @@
|
||||
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',
|
||||
|
Loading…
Reference in New Issue
Block a user