Browse Source

Working on new query editor for influxdb 0.9, looking good! #1525

Torkel Ödegaard 10 years ago
parent
commit
5ca8d590bd

+ 2 - 1
public/app/directives/metric.segment.js

@@ -14,7 +14,8 @@ function (angular, app, _, $) {
                             ' class="tight-form-clear-input input-medium"' +
                             ' class="tight-form-clear-input input-medium"' +
                             ' spellcheck="false" style="display:none"></input>';
                             ' spellcheck="false" style="display:none"></input>';
 
 
-      var buttonTemplate = '<a class="tight-form-item" tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
+      var buttonTemplate = '<a class="tight-form-item" ng-class="segment.cssClass" ' +
+        'tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
 
 
       return {
       return {
         scope: {
         scope: {

+ 7 - 6
public/app/plugins/datasource/influxdb/partials/query.editor.html

@@ -83,15 +83,16 @@
 				<li class="tight-form-item query-keyword">
 				<li class="tight-form-item query-keyword">
 					WHERE
 					WHERE
 				</li>
 				</li>
-				<li>
-					<input type="text" class="input-medium tight-form-input" ng-model="target.condition"
-					  bs-tooltip="'Add a where clause'" data-placement="right" spellcheck='false' placeholder="column ~= value" ng-blur="get_data()">
+				<li ng-repeat="segment in tagSegments">
+					<metric-segment segment="segment" get-alt-segments="getTagsOrValues(segment, $index)" on-value-changed="tagSegmentUpdated(segment, $index)"></metric-segment>
 				</li>
 				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					<span class="query-keyword">GROUP BY</span>
 					<span class="query-keyword">GROUP BY</span>
-					time($interval), <i class="fa fa-plus"></i>
+					time($interval)
+				</li>
+				<li ng-repeat="segment in groupBySegments">
+					<metric-segment segment="segment" get-alt-segments="getTagsOrValues(segment, 0)" on-value-changed="groupByTagUpdated(segment, $index)"></metric-segment>
 				</li>
 				</li>
-
 				<li class="dropdown">
 				<li class="dropdown">
 					<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
 					<a class="tight-form-item pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
 						<span ng-show="target.fill">
 						<span ng-show="target.fill">
@@ -106,7 +107,7 @@
 						<li><a ng-click="target.fill = 'null'">fill (null)</a></li>
 						<li><a ng-click="target.fill = 'null'">fill (null)</a></li>
 						<li><a ng-click="target.fill = '0'">fill (0)</a></li>
 						<li><a ng-click="target.fill = '0'">fill (0)</a></li>
 					</ul>
 					</ul>
-				</li>
+			</li>
 
 
       </ul>
       </ul>
 
 

+ 2 - 2
public/app/plugins/datasource/influxdb/queryBuilder.js

@@ -33,8 +33,8 @@ function (_) {
 
 
     query +=  aggregationFunc + '(value)';
     query +=  aggregationFunc + '(value)';
     query += ' FROM ' + measurement + ' WHERE $timeFilter';
     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('');
     }).join('');
 
 
     query += ' GROUP BY time($interval)';
     query += ' GROUP BY time($interval)';

+ 139 - 42
public/app/plugins/datasource/influxdb/queryCtrl.js

@@ -7,13 +7,11 @@ function (angular, _) {
 
 
   var module = angular.module('grafana.controllers');
   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 = [
     $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) {
     $scope.functionMenu = _.map($scope.functionList, function(func) {
@@ -23,12 +21,41 @@ function (angular, _) {
     $scope.init = function() {
     $scope.init = function() {
       var target = $scope.target;
       var target = $scope.target;
       target.function = target.function || 'mean';
       target.function = target.function || 'mean';
+      target.tags = target.tags || [];
+      target.groupByTags = target.groupByTags || [];
 
 
       if (!target.measurement) {
       if (!target.measurement) {
         $scope.measurementSegment = MetricSegment.newSelectMeasurement();
         $scope.measurementSegment = MetricSegment.newSelectMeasurement();
       } else {
       } else {
         $scope.measurementSegment = new MetricSegment(target.measurement);
         $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) {
     $scope.changeFunction = function(func) {
@@ -56,43 +83,107 @@ function (angular, _) {
     };
     };
 
 
     $scope.getMeasurements = function () {
     $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 });
-        });
-
-        _.each(templateSrv.variables, function(variable) {
-          measurements.unshift(new MetricSegment({
-            type: 'template',
-            value: '$' + variable.name,
-            expandable: true,
-          }));
-        });
-
-        return measurements;
-      }, function(err) {
-        $scope.parserError = err.message || 'Failed to issue metric query';
-        return [];
+      return $scope.datasource.metricFindQuery('SHOW MEASUREMENTS', 'MEASUREMENTS')
+      .then($scope.transformToSegments)
+      .then($scope.addTemplateVariableSegments)
+      .then(null, $scope.handleQueryError);
+    };
+
+    $scope.handleQueryError = 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) {
     function MetricSegment(options) {
       if (options === '*' || options.value === '*') {
       if (options === '*' || options.value === '*') {
         this.value = '*';
         this.value = '*';
@@ -107,19 +198,25 @@ function (angular, _) {
         return;
         return;
       }
       }
 
 
+      this.cssClass = options.cssClass;
+      this.type = options.type;
       this.fake = options.fake;
       this.fake = options.fake;
       this.value = options.value;
       this.value = options.value;
       this.type = options.type;
       this.type = options.type;
       this.expandable = options.expandable;
       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() {
     MetricSegment.newSelectMeasurement = function() {
       return new MetricSegment({value: 'select measurement', fake: true});
       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: '<i class="fa fa-plus"></i>', type: 'plus-button' });
     };
     };
 
 
     MetricSegment.newSelectTagValue = function() {
     MetricSegment.newSelectTagValue = function() {

+ 1 - 1
public/test/specs/influx09-querybuilder-specs.js

@@ -21,7 +21,7 @@ define([
     describe('series with tags only', function() {
     describe('series with tags only', function() {
       var builder = new InfluxQueryBuilder({
       var builder = new InfluxQueryBuilder({
         measurement: 'cpu',
         measurement: 'cpu',
-        tags: {'hostname': 'server1'}
+        tags: [{key: 'hostname', value: 'server1'}]
       });
       });
 
 
       var query = builder.build();
       var query = builder.build();

+ 120 - 0
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');
+      });
+    });
+
+  });
+});

+ 1 - 0
public/test/test-main.js

@@ -128,6 +128,7 @@ require([
     'specs/influxQueryBuilder-specs',
     'specs/influxQueryBuilder-specs',
     'specs/influx09-querybuilder-specs',
     'specs/influx09-querybuilder-specs',
     'specs/influxdb-datasource-specs',
     'specs/influxdb-datasource-specs',
+    'specs/influxdbQueryCtrl-specs',
     'specs/graph-ctrl-specs',
     'specs/graph-ctrl-specs',
     'specs/graph-specs',
     'specs/graph-specs',
     'specs/graph-tooltip-specs',
     'specs/graph-tooltip-specs',