diff --git a/public/app/directives/metric.segment.js b/public/app/directives/metric.segment.js
index 5510dea6657..05bf7e485e3 100644
--- a/public/app/directives/metric.segment.js
+++ b/public/app/directives/metric.segment.js
@@ -14,7 +14,8 @@ function (angular, app, _, $) {
' class="tight-form-clear-input input-medium"' +
' spellcheck="false" style="display:none">';
- var buttonTemplate = '';
+ var buttonTemplate = '';
return {
scope: {
diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html
index 7210cfc24dc..494eaff5520 100644
--- a/public/app/plugins/datasource/influxdb/partials/query.editor.html
+++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html
@@ -83,15 +83,16 @@
WHERE
-
-
+
+
GROUP BY
- time($interval),
+ time($interval)
+
+
+
-
@@ -106,7 +107,7 @@
fill (null)
fill (0)
-
+
diff --git a/public/app/plugins/datasource/influxdb/queryBuilder.js b/public/app/plugins/datasource/influxdb/queryBuilder.js
index e8eaefda61a..9317cea4574 100644
--- a/public/app/plugins/datasource/influxdb/queryBuilder.js
+++ b/public/app/plugins/datasource/influxdb/queryBuilder.js
@@ -33,8 +33,8 @@ function (_) {
query += aggregationFunc + '(value)';
query += ' FROM ' + measurement + ' WHERE $timeFilter';
- query += _.map(target.tags, function(value, key) {
- return ' AND ' + key + '=' + "'" + value + "'";
+ query += _.map(target.tags, function(tag) {
+ return ' AND ' + tag.key + '=' + "'" + tag.value + "'";
}).join('');
query += ' GROUP BY time($interval)';
diff --git a/public/app/plugins/datasource/influxdb/queryCtrl.js b/public/app/plugins/datasource/influxdb/queryCtrl.js
index e78c100b5c5..5aeb68f00e7 100644
--- a/public/app/plugins/datasource/influxdb/queryCtrl.js
+++ b/public/app/plugins/datasource/influxdb/queryCtrl.js
@@ -7,13 +7,11 @@ function (angular, _) {
var module = angular.module('grafana.controllers');
- module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv) {
+ module.controller('InfluxQueryCtrl', function($scope, $timeout, $sce, templateSrv, $q) {
$scope.functionList = [
- 'count', 'mean', 'sum', 'min',
- 'max', 'mode', 'distinct', 'median',
- 'derivative', 'stddev', 'first', 'last',
- 'difference'
+ 'count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
+ 'derivative', 'stddev', 'first', 'last', 'difference'
];
$scope.functionMenu = _.map($scope.functionList, function(func) {
@@ -23,12 +21,41 @@ function (angular, _) {
$scope.init = function() {
var target = $scope.target;
target.function = target.function || 'mean';
+ target.tags = target.tags || [];
+ target.groupByTags = target.groupByTags || [];
if (!target.measurement) {
$scope.measurementSegment = MetricSegment.newSelectMeasurement();
} else {
$scope.measurementSegment = new MetricSegment(target.measurement);
}
+
+ $scope.tagSegments = [];
+ _.each(target.tags, function(tag) {
+ if (tag.condition) {
+ $scope.tagSegments.push(MetricSegment.newCondition(tag.condition));
+ }
+ $scope.tagSegments.push(new MetricSegment({value: tag.key, type: 'key' }));
+ $scope.tagSegments.push(new MetricSegment({fake: true, value: "="}));
+ $scope.tagSegments.push(new MetricSegment({value: tag.value, type: 'value'}));
+ });
+
+ if ($scope.tagSegments.length % 3 === 0) {
+ $scope.tagSegments.push(MetricSegment.newPlusButton());
+ }
+
+ $scope.groupBySegments = [];
+ _.each(target.groupByTags, function(tag) {
+ $scope.groupBySegments.push(new MetricSegment(tag));
+ });
+
+ $scope.groupBySegments.push(MetricSegment.newPlusButton());
+ };
+
+ $scope.groupByTagUpdated = function(segment, index) {
+ if (index === $scope.groupBySegments.length-1) {
+ $scope.groupBySegments.push(MetricSegment.newPlusButton());
+ }
};
$scope.changeFunction = function(func) {
@@ -56,43 +83,107 @@ function (angular, _) {
};
$scope.getMeasurements = function () {
- // var measurement = $scope.segments[0].value;
- // var queryType, query;
- // if (index === 0) {
- // queryType = 'MEASUREMENTS';
- // query = 'SHOW MEASUREMENTS';
- // } else if (index % 2 === 1) {
- // queryType = 'TAG_KEYS';
- // query = 'SHOW TAG KEYS FROM "' + measurement + '"';
- // } else {
- // queryType = 'TAG_VALUES';
- // query = 'SHOW TAG VALUES FROM "' + measurement + '" WITH KEY = ' + $scope.segments[$scope.segments.length - 2].value;
- // }
- //
- // console.log('getAltSegments: query' , query);
- //
- console.log('get measurements');
- return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS').then(function(results) {
- console.log('get alt segments: response', results);
- var measurements = _.map(results, function(segment) {
- return new MetricSegment({ value: segment.text, expandable: segment.expandable });
- });
+ return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS')
+ .then($scope.transformToSegments)
+ .then($scope.addTemplateVariableSegments)
+ .then(null, $scope.handleQueryError);
+ };
- _.each(templateSrv.variables, function(variable) {
- measurements.unshift(new MetricSegment({
- type: 'template',
- value: '$' + variable.name,
- expandable: true,
- }));
- });
+ $scope.handleQueryError = function(err) {
+ $scope.parserError = err.message || 'Failed to issue metric query';
+ return [];
+ };
- return measurements;
- }, function(err) {
- $scope.parserError = err.message || 'Failed to issue metric query';
- return [];
+ $scope.transformToSegments = function(results) {
+ return _.map(results, function(segment) {
+ return new MetricSegment({ value: segment.text, expandable: segment.expandable });
});
};
+ $scope.addTemplateVariableSegments = function(segments) {
+ _.each(templateSrv.variables, function(variable) {
+ segments.unshift(new MetricSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
+ });
+ return segments;
+ };
+
+ $scope.getTagsOrValues = function(segment, index) {
+ var query, queryType;
+ if (segment.type === 'key' || segment.type === 'plus-button') {
+ queryType = 'TAG_KEYS';
+ query = 'SHOW TAG KEYS FROM "' + $scope.target.measurement + '"';
+ } else if (segment.type === 'value') {
+ queryType = 'TAG_VALUES';
+ query = 'SHOW TAG VALUES FROM "' + $scope.target.measurement + '" WITH KEY = ' + $scope.tagSegments[index-2].value;
+ } else if (segment.type === 'condition') {
+ return $q.when([new MetricSegment('AND'), new MetricSegment('OR')]);
+ }
+ else {
+ return $q.when([]);
+ }
+
+ return $scope.datasource.metricFindQuery(query, queryType)
+ .then($scope.transformToSegments)
+ .then($scope.addTemplateVariableSegments)
+ .then(function(results) {
+ if (queryType === 'TAG_KEYS' && segment.type !== 'plus-button') {
+ results.push(new MetricSegment({fake: true, value: 'remove tag filter'}));
+ }
+ return results;
+ })
+ .then(null, $scope.handleQueryError);
+ };
+
+ $scope.tagSegmentUpdated = function(segment, index) {
+ $scope.tagSegments[index] = segment;
+
+ if (segment.value === 'remove tag filter') {
+ $scope.tagSegments.splice(index, 3);
+ if ($scope.tagSegments.length === 0) {
+ $scope.tagSegments.push(MetricSegment.newPlusButton());
+ } else {
+ $scope.tagSegments.splice(index-1, 1);
+ $scope.tagSegments.push(MetricSegment.newPlusButton());
+ }
+ }
+ else {
+ if (segment.type === 'plus-button') {
+ if (index > 2) {
+ $scope.tagSegments.splice(index, 0, MetricSegment.newCondition('AND'));
+ }
+ $scope.tagSegments.push(new MetricSegment({fake: true, value: '=', type: 'operator'}));
+ $scope.tagSegments.push(new MetricSegment({fake: true, value: 'select tag value', type: 'value' }));
+ segment.type = 'key';
+ }
+
+ if ((index+1) === $scope.tagSegments.length) {
+ $scope.tagSegments.push(MetricSegment.newPlusButton());
+ }
+ }
+
+ $scope.rebuildTargetTagConditions();
+ };
+
+ $scope.rebuildTargetTagConditions = function() {
+ var tags = [{}];
+ var tagIndex = 0;
+ _.each($scope.tagSegments, function(segment2) {
+ if (segment2.type === 'key') {
+ tags[tagIndex].key = segment2.value;
+ }
+ else if (segment2.type === 'value') {
+ tags[tagIndex].value = segment2.value;
+ }
+ else if (segment2.type === 'condition') {
+ tags.push({ condition: segment2.value });
+ tagIndex += 1;
+ }
+ });
+
+ $scope.target.tags = tags;
+ $scope.$parent.get_data();
+ };
+
function MetricSegment(options) {
if (options === '*' || options.value === '*') {
this.value = '*';
@@ -107,19 +198,25 @@ function (angular, _) {
return;
}
+ this.cssClass = options.cssClass;
+ this.type = options.type;
this.fake = options.fake;
this.value = options.value;
this.type = options.type;
this.expandable = options.expandable;
- this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
+ this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
MetricSegment.newSelectMeasurement = function() {
return new MetricSegment({value: 'select measurement', fake: true});
};
- MetricSegment.newSelectTag = function() {
- return new MetricSegment({value: 'select tag', fake: true});
+ MetricSegment.newCondition = function(condition) {
+ return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
+ };
+
+ MetricSegment.newPlusButton = function() {
+ return new MetricSegment({fake: true, html: '', type: 'plus-button' });
};
MetricSegment.newSelectTagValue = function() {
diff --git a/public/test/specs/influx09-querybuilder-specs.js b/public/test/specs/influx09-querybuilder-specs.js
index 09fb343171a..bec2035760f 100644
--- a/public/test/specs/influx09-querybuilder-specs.js
+++ b/public/test/specs/influx09-querybuilder-specs.js
@@ -21,7 +21,7 @@ define([
describe('series with tags only', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
- tags: {'hostname': 'server1'}
+ tags: [{key: 'hostname', value: 'server1'}]
});
var query = builder.build();
diff --git a/public/test/specs/influxdbQueryCtrl-specs.js b/public/test/specs/influxdbQueryCtrl-specs.js
new file mode 100644
index 00000000000..0e177a6aebe
--- /dev/null
+++ b/public/test/specs/influxdbQueryCtrl-specs.js
@@ -0,0 +1,120 @@
+define([
+ 'helpers',
+ 'plugins/datasource/influxdb/queryCtrl'
+], function(helpers) {
+ 'use strict';
+
+ describe('InfluxDBQueryCtrl', function() {
+ var ctx = new helpers.ControllerTestContext();
+
+ beforeEach(module('grafana.controllers'));
+ beforeEach(ctx.providePhase());
+ beforeEach(ctx.createControllerPhase('InfluxQueryCtrl'));
+
+ beforeEach(function() {
+ ctx.scope.target = {};
+ ctx.scope.$parent = { get_data: sinon.spy() };
+
+ ctx.scope.datasource = ctx.datasource;
+ ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
+ });
+
+ describe('init', function() {
+ beforeEach(function() {
+ ctx.scope.init();
+ });
+
+ it('should init tagSegments', function() {
+ expect(ctx.scope.tagSegments.length).to.be(1);
+ });
+
+ it('should init measurementSegment', function() {
+ expect(ctx.scope.measurementSegment.value).to.be('select measurement');
+ });
+ });
+
+ describe('when first tag segment is updated', function() {
+ beforeEach(function() {
+ ctx.scope.init();
+ ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
+ });
+
+ it('should update tag key', function() {
+ expect(ctx.scope.target.tags[0].key).to.be('asd');
+ expect(ctx.scope.tagSegments[0].type).to.be('key');
+ });
+
+ it('should add tagSegments', function() {
+ expect(ctx.scope.tagSegments.length).to.be(3);
+ });
+ });
+
+ describe('when last tag value segment is updated', function() {
+ beforeEach(function() {
+ ctx.scope.init();
+ ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
+ ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+ });
+
+ it('should update tag value', function() {
+ expect(ctx.scope.target.tags[0].value).to.be('server1');
+ });
+
+ it('should add plus button for another filter', function() {
+ expect(ctx.scope.tagSegments[3].fake).to.be(true);
+ });
+ });
+
+ describe('when second tag key is added', function() {
+ beforeEach(function() {
+ ctx.scope.init();
+ ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+ ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+ ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+ });
+
+ it('should update tag key', function() {
+ expect(ctx.scope.target.tags[1].key).to.be('key2');
+ });
+
+ it('should add AND segment', function() {
+ expect(ctx.scope.tagSegments[3].value).to.be('AND');
+ });
+ });
+
+ describe('when condition is changed', function() {
+ beforeEach(function() {
+ ctx.scope.init();
+ ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+ ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+ ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+ ctx.scope.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3);
+ });
+
+ it('should update tag condition', function() {
+ expect(ctx.scope.target.tags[1].condition).to.be('OR');
+ });
+
+ it('should update AND segment', function() {
+ expect(ctx.scope.tagSegments[3].value).to.be('OR');
+ expect(ctx.scope.tagSegments.length).to.be(7);
+ });
+ });
+
+ describe('when deleting is changed', function() {
+ beforeEach(function() {
+ ctx.scope.init();
+ ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+ ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+ ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+ ctx.scope.tagSegmentUpdated({value: 'remove tag filter', type: 'key'}, 4);
+ });
+
+ it('should remove all segment after 2 and replace with plus button', function() {
+ expect(ctx.scope.tagSegments.length).to.be(4);
+ expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
+ });
+ });
+
+ });
+});
diff --git a/public/test/test-main.js b/public/test/test-main.js
index 3e944c3c36f..86196069265 100644
--- a/public/test/test-main.js
+++ b/public/test/test-main.js
@@ -128,6 +128,7 @@ require([
'specs/influxQueryBuilder-specs',
'specs/influx09-querybuilder-specs',
'specs/influxdb-datasource-specs',
+ 'specs/influxdbQueryCtrl-specs',
'specs/graph-ctrl-specs',
'specs/graph-specs',
'specs/graph-tooltip-specs',