From 5ca8d590bd9a769397afc24de5529f8b1c9ad429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 May 2015 15:58:07 +0200 Subject: [PATCH] Working on new query editor for influxdb 0.9, looking good! #1525 --- public/app/directives/metric.segment.js | 3 +- .../influxdb/partials/query.editor.html | 13 +- .../datasource/influxdb/queryBuilder.js | 4 +- .../plugins/datasource/influxdb/queryCtrl.js | 177 ++++++++++++++---- .../test/specs/influx09-querybuilder-specs.js | 2 +- public/test/specs/influxdbQueryCtrl-specs.js | 120 ++++++++++++ public/test/test-main.js | 1 + 7 files changed, 270 insertions(+), 50 deletions(-) create mode 100644 public/test/specs/influxdbQueryCtrl-specs.js 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) +
  • +
  • +
  • -
  • 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',