Browse Source

feat(influxdb editor): lots of work on new editor, #2856

Torkel Ödegaard 10 years ago
parent
commit
83052352dc

+ 141 - 0
public/app/plugins/datasource/influxdb/influx_query.ts

@@ -0,0 +1,141 @@
+///<reference path="../../../headers/common.d.ts" />
+///<amd-dependency path="./query_builder" name="InfluxQueryBuilder" />
+
+import _ = require('lodash');
+import queryPart = require('./query_part');
+
+declare var InfluxQueryBuilder: any;
+
+class InfluxQuery {
+  target: any;
+  selectParts: any[];
+  groupByParts: any;
+  queryBuilder: any;
+
+  constructor(target) {
+    this.target = target;
+
+    target.tags = target.tags || [];
+    target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
+    target.select = target.select || [[
+      {name: 'mean', params: ['value']},
+    ]];
+
+    this.updateSelectParts();
+    this.groupByParts = [
+      queryPart.create({name: 'time', params: ['$interval']})
+    ];
+  }
+
+  updateSelectParts() {
+    this.selectParts = _.map(this.target.select, function(parts: any) {
+      return _.map(parts, function(part: any) {
+        return queryPart.create(part);
+      });
+    });
+  }
+
+  removeSelect(index: number) {
+    this.target.select.splice(index, 1);
+    this.updateSelectParts();
+  }
+
+  addSelect() {
+    this.target.select.push([
+      {name: 'mean', params: ['value']},
+    ]);
+    this.updateSelectParts();
+  }
+
+  private renderTagCondition(tag, index) {
+    var str = "";
+    var operator = tag.operator;
+    var value = tag.value;
+    if (index > 0) {
+      str = (tag.condition || 'AND') + ' ';
+    }
+
+    if (!operator) {
+      if (/^\/.*\/$/.test(tag.value)) {
+        operator = '=~';
+      } else {
+        operator = '=';
+      }
+    }
+
+    // quote value unless regex
+    if (operator !== '=~' && operator !== '!~') {
+      value = "'" + value + "'";
+    }
+
+    return str + '"' + tag.key + '" ' + operator + ' ' + value;
+  }
+
+  private getGroupByTimeInterval(interval) {
+    if (interval === 'auto') {
+      return '$interval';
+    }
+    return interval;
+  }
+
+  render() {
+    var target = this.target;
+
+    if (!target.measurement) {
+      throw "Metric measurement is missing";
+    }
+
+    if (!target.fields) {
+      target.fields = [{name: 'value', func: target.function || 'mean'}];
+    }
+
+    var query = 'SELECT ';
+    var i, y;
+    for (i = 0; i < this.selectParts.length; i++) {
+      let parts = this.selectParts[i];
+      var selectText = "";
+      for (y = 0; y < parts.length; y++) {
+        let part = parts[y];
+        selectText = part.render(selectText);
+      }
+
+      if (i > 0) {
+        query += ', ';
+      }
+      query += selectText;
+    }
+
+    var measurement = target.measurement;
+    if (!measurement.match('^/.*/') && !measurement.match(/^merge\(.*\)/)) {
+      measurement = '"' + measurement+ '"';
+    }
+
+    query += ' FROM ' + measurement + ' WHERE ';
+    var conditions = _.map(target.tags, (tag, index) => {
+      return this.renderTagCondition(tag, index);
+    });
+
+    query += conditions.join(' ');
+    query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
+
+    query += ' GROUP BY';
+    for (i = 0; i < target.groupBy.length; i++) {
+      var group = target.groupBy[i];
+      if (group.type === 'time') {
+        query += ' time(' + this.getGroupByTimeInterval(group.interval) + ')';
+      } else {
+        query += ', "' + group.key + '"';
+      }
+    }
+
+    if (target.fill) {
+      query += ' fill(' + target.fill + ')';
+    }
+
+    target.query = query;
+
+    return query;
+  }
+}
+
+export = InfluxQuery;

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

@@ -65,7 +65,7 @@
 
 	<div ng-hide="target.rawQuery">
 
-		<div class="tight-form" ng-repeat="parts in selectParts">
+		<div class="tight-form" ng-repeat="parts in queryModel.selectParts">
 			<ul class="tight-form-list">
 				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
 					<span ng-show="$index === 0">SELECT</span>
@@ -85,13 +85,13 @@
 			<div class="clearfix"></div>
 		</div>
 
-		<div class="tight-form" ng-repeat="groupBy in target.groupBy">
+		<div class="tight-form" ng-repeat="part in queryModel.groupByParts">
 			<ul class="tight-form-list">
 				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
 					<span ng-show="$index === 0">GROUP BY</span>
 				</li>
-				<li ng-if="groupBy.type === 'time'">
-					<influx-query-part-editor part="groupByParts" class="tight-form-item tight-form-func"></influx-query-part-editor>
+				<li>
+					<influx-query-part-editor part="part" class="tight-form-item tight-form-func"></influx-query-part-editor>
 				</li>
 				<!-- <li class="dropdown" ng&#45;if="groupBy.type === 'time'"> -->
 					<!-- 	<a class="tight&#45;form&#45;item pointer" data&#45;toggle="dropdown" bs&#45;tooltip="'Insert missing values, important when stacking'" data&#45;placement="right"> -->

+ 2 - 1
public/app/plugins/datasource/influxdb/query_builder.js

@@ -4,8 +4,9 @@ define([
 function (_) {
   'use strict';
 
-  function InfluxQueryBuilder(target) {
+  function InfluxQueryBuilder(target, queryModel) {
     this.target = target;
+    this.model = queryModel;
 
     if (target.groupByTags) {
       target.groupBy = [{type: 'time', interval: 'auto'}];

+ 10 - 67
public/app/plugins/datasource/influxdb/query_ctrl.js

@@ -2,10 +2,10 @@ define([
   'angular',
   'lodash',
   './query_builder',
-  './query_part',
+  './influx_query',
   './query_part_editor',
 ],
-function (angular, _, InfluxQueryBuilder, queryPart) {
+function (angular, _, InfluxQueryBuilder, InfluxQuery) {
   'use strict';
 
   var module = angular.module('grafana.controllers');
@@ -15,29 +15,18 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
     $scope.init = function() {
       if (!$scope.target) { return; }
 
-      var target = $scope.target;
-      target.tags = target.tags || [];
-      target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
-      target.fields = target.fields || [{name: 'value'}];
-      target.select = target.select || [[
-        {name: 'field', params: ['value']},
-        {name: 'mean', params: []},
-      ]];
+      $scope.target = $scope.target;
+      $scope.queryModel = new InfluxQuery($scope.target);
+      $scope.queryBuilder = new InfluxQueryBuilder($scope.target);
 
-      $scope.updateSelectParts();
-
-      $scope.groupByParts = queryPart.create({name: 'time', params:['$interval']});
-
-      $scope.queryBuilder = new InfluxQueryBuilder(target);
-
-      if (!target.measurement) {
+      if (!$scope.target.measurement) {
         $scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
       } else {
-        $scope.measurementSegment = uiSegmentSrv.newSegment(target.measurement);
+        $scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
       }
 
       $scope.tagSegments = [];
-      _.each(target.tags, function(tag) {
+      _.each($scope.target.tags, function(tag) {
         if (!tag.operator) {
           if (/^\/.*\/$/.test(tag.value)) {
             tag.operator = "=~";
@@ -78,32 +67,14 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
     };
 
     $scope.addSelect = function() {
-      $scope.target.select.push([
-        {name: 'field', params: ['value']},
-        {name: 'mean', params: []},
-      ]);
-      $scope.updateSelectParts();
+      $scope.queryModel.addSelect();
     };
 
     $scope.removeSelect = function(index) {
-      $scope.target.select.splice(index, 1);
-      $scope.updateSelectParts();
+      $scope.queryModel.removeSelect(index);
       $scope.get_data();
     };
 
-    $scope.updateSelectParts = function() {
-      $scope.selectParts = _.map($scope.target.select, function(parts) {
-        return _.map(parts, function(part) {
-          return queryPart.create(part);
-        });
-      });
-    };
-
-    $scope.changeFunction = function(func) {
-      $scope.target.function = func;
-      $scope.$parent.get_data();
-    };
-
     $scope.measurementChanged = function() {
       $scope.target.measurement = $scope.measurementSegment.value;
       $scope.$parent.get_data();
@@ -125,22 +96,6 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
       .then($scope.transformToSegments(true), $scope.handleQueryError);
     };
 
-    $scope.getFunctions = function () {
-      var functionList = ['count', 'mean', 'sum', 'min', 'max', 'mode', 'distinct', 'median',
-        'stddev', 'first', 'last'
-      ];
-      return $q.when(_.map(functionList, function(func) {
-        return uiSegmentSrv.newSegment(func);
-      }));
-    };
-
-    $scope.getGroupByTimeIntervals = function () {
-      var times = ['auto', '1s', '10s', '1m', '2m', '5m', '10m', '30m', '1h', '1d'];
-      return $q.when(_.map(times, function(func) {
-        return uiSegmentSrv.newSegment(func);
-      }));
-    };
-
     $scope.handleQueryError = function(err) {
       $scope.parserError = err.message || 'Failed to issue metric query';
       return [];
@@ -202,18 +157,6 @@ function (angular, _, InfluxQueryBuilder, queryPart) {
       .then(null, $scope.handleQueryError);
     };
 
-    $scope.addField = function() {
-      $scope.target.fields.push({name: $scope.addFieldSegment.value, func: 'mean'});
-      _.extend($scope.addFieldSegment, uiSegmentSrv.newPlusButton());
-    };
-
-    $scope.fieldChanged = function(field) {
-      if (field.name === '-- remove from select --') {
-        $scope.target.fields = _.without($scope.target.fields, field);
-      }
-      $scope.get_data();
-    };
-
     $scope.getTagOptions = function() {
       var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
 

+ 49 - 31
public/app/plugins/datasource/influxdb/query_part.ts

@@ -15,11 +15,13 @@ class QueryPartDef {
   name: string;
   params: any[];
   defaultParams: any[];
+  renderer: any;
 
   constructor(options: any) {
     this.name = options.name;
     this.params = options.params;
     this.defaultParams = options.defaultParams;
+    this.renderer = options.renderer;
   }
 
   static register(options: any) {
@@ -27,25 +29,55 @@ class QueryPartDef {
   }
 }
 
-QueryPartDef.register({
-  name: 'field',
-  category: categories.Transform,
-  params: [{type: 'field'}],
-  defaultParams: ['value'],
-});
+function functionRenderer(part, innerExpr) {
+  var str = part.def.name + '(';
+  var parameters = _.map(part.params, (value, index) => {
+    var paramType = part.def.params[index];
+    if (paramType.quote === 'single') {
+      return "'" + value + "'";
+    } else if (paramType.quote === 'double') {
+      return '"' + value + '"';
+    }
+
+    return value;
+  });
+
+  if (innerExpr) {
+    parameters.unshift(innerExpr);
+  }
+  return str + parameters.join(', ') + ')';
+}
+
+function aliasRenderer(part, innerExpr) {
+  return innerExpr + ' AS ' + '"' + part.params[0] + '"';
+}
+
+function suffixRenderer(part, innerExpr) {
+  return innerExpr + ' ' + part.params[0];
+}
+
+function identityRenderer(part, innerExpr) {
+  return part.params[0];
+}
+
+function quotedIdentityRenderer(part, innerExpr) {
+  return '"' + part.params[0] + '"';
+}
 
 QueryPartDef.register({
   name: 'mean',
   category: categories.Transform,
-  params: [],
-  defaultParams: [],
+  params: [{type: 'field', quote: 'double'}],
+  defaultParams: ['value'],
+  renderer: functionRenderer,
 });
 
 QueryPartDef.register({
-  name: 'derivate',
+  name: 'derivative',
   category: categories.Transform,
-  params: [{ name: "rate", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
+  params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}],
   defaultParams: ['10s'],
+  renderer: functionRenderer,
 });
 
 QueryPartDef.register({
@@ -53,6 +85,7 @@ QueryPartDef.register({
   category: categories.Transform,
   params: [{ name: "rate", type: "interval", options: ['$interval', '1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
   defaultParams: ['$interval'],
+  renderer: functionRenderer,
 });
 
 QueryPartDef.register({
@@ -60,13 +93,16 @@ QueryPartDef.register({
   category: categories.Transform,
   params: [{ name: "expr", type: "string"}],
   defaultParams: [' / 100'],
+  renderer: suffixRenderer,
 });
 
 QueryPartDef.register({
   name: 'alias',
   category: categories.Transform,
-  params: [{ name: "name", type: "string"}],
+  params: [{ name: "name", type: "string", quote: 'double'}],
   defaultParams: ['alias'],
+  renderMode: 'suffix',
+  renderer: aliasRenderer,
 });
 
 class QueryPart {
@@ -83,29 +119,11 @@ class QueryPart {
     }
 
     this.params = part.params || _.clone(this.def.defaultParams);
+    this.updateText();
   }
 
   render(innerExpr: string) {
-    var str = this.def.name + '(';
-    var parameters = _.map(this.params, (value, index) => {
-
-      var paramType = this.def.params[index].type;
-      if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
-        return value;
-      }
-      else if (paramType === 'int_or_interval' && _.isNumber(value)) {
-        return value;
-      }
-
-      return "'" + value + "'";
-
-    });
-
-    if (innerExpr) {
-      parameters.unshift(innerExpr);
-    }
-
-    return str + parameters.join(', ') + ')';
+    return this.def.renderer(this, innerExpr);
   }
 
   hasMultipleParamsInString (strValue, index) {

+ 36 - 0
public/app/plugins/datasource/influxdb/specs/influx_query_specs.ts

@@ -0,0 +1,36 @@
+import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
+
+import InfluxQuery = require('../influx_query');
+
+describe.only('InfluxQuery', function() {
+
+  describe('series with mesurement only', function() {
+    it('should generate correct query', function() {
+      var query = new InfluxQuery({
+        measurement: 'cpu',
+      });
+
+      var queryText = query.render();
+      expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
+    });
+  });
+
+  describe('series with math and alias', function() {
+    it('should generate correct query', function() {
+      var query = new InfluxQuery({
+        measurement: 'cpu',
+        select: [
+          [
+            {name: 'mean', params: ['value']},
+            {name: 'math', params: ['/100']},
+            {name: 'alias', params: ['text']},
+          ]
+        ]
+      });
+
+      var queryText = query.render();
+      expect(queryText).to.be('SELECT mean("value") /100 AS "text" FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
+    });
+  });
+
+});

+ 34 - 32
public/app/plugins/datasource/influxdb/specs/query_builder_specs.ts

@@ -9,8 +9,8 @@ describe('InfluxQueryBuilder', function() {
   describe('series with mesurement only', function() {
     it('should generate correct query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      groupBy: [{type: 'time', interval: 'auto'}]
+        measurement: 'cpu',
+        groupBy: [{type: 'time', interval: 'auto'}]
       });
 
       var query = builder.build();
@@ -22,9 +22,9 @@ describe('InfluxQueryBuilder', function() {
   describe('series with math expr and as expr', function() {
     it('should generate correct query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}],
-      groupBy: [{type: 'time', interval: 'auto'}]
+        measurement: 'cpu',
+        fields: [{name: 'test', func: 'max', mathExpr: '*2', asExpr: 'new_name'}],
+        groupBy: [{type: 'time', interval: 'auto'}]
       });
 
       var query = builder.build();
@@ -36,22 +36,22 @@ describe('InfluxQueryBuilder', function() {
   describe('series with single tag only', function() {
     it('should generate correct query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      groupBy: [{type: 'time', interval: 'auto'}],
-      tags: [{key: 'hostname', value: 'server1'}]
+        measurement: 'cpu',
+        groupBy: [{type: 'time', interval: 'auto'}],
+        tags: [{key: 'hostname', value: 'server1'}]
       });
 
       var query = builder.build();
 
       expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND $timeFilter'
-          + ' GROUP BY time($interval)');
+                          + ' GROUP BY time($interval)');
     });
 
     it('should switch regex operator with tag value is regex', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      groupBy: [{type: 'time', interval: 'auto'}],
-      tags: [{key: 'app', value: '/e.*/'}]
+        measurement: 'cpu',
+        groupBy: [{type: 'time', interval: 'auto'}],
+        tags: [{key: 'app', value: '/e.*/'}]
       });
 
       var query = builder.build();
@@ -62,57 +62,57 @@ describe('InfluxQueryBuilder', function() {
   describe('series with multiple fields', function() {
     it('should generate correct query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      tags: [],
-      groupBy: [{type: 'time', interval: 'auto'}],
-      fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }]
+        measurement: 'cpu',
+        tags: [],
+        groupBy: [{type: 'time', interval: 'auto'}],
+        fields: [{ name: 'tx_in', func: 'sum' }, { name: 'tx_out', func: 'mean' }]
       });
 
       var query = builder.build();
       expect(query).to.be('SELECT sum("tx_in") AS "tx_in", mean("tx_out") AS "tx_out" ' +
-          'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
+                          'FROM "cpu" WHERE $timeFilter GROUP BY time($interval)');
     });
   });
 
   describe('series with multiple tags only', function() {
     it('should generate correct query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      groupBy: [{type: 'time', interval: 'auto'}],
-      tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
+        measurement: 'cpu',
+        groupBy: [{type: 'time', interval: 'auto'}],
+        tags: [{key: 'hostname', value: 'server1'}, {key: 'app', value: 'email', condition: "AND"}]
       });
 
       var query = builder.build();
       expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
-          '$timeFilter GROUP BY time($interval)');
+                          '$timeFilter GROUP BY time($interval)');
     });
   });
 
   describe('series with tags OR condition', function() {
     it('should generate correct query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      groupBy: [{type: 'time', interval: 'auto'}],
-      tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
+        measurement: 'cpu',
+        groupBy: [{type: 'time', interval: 'auto'}],
+        tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}]
       });
 
       var query = builder.build();
       expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
-          '$timeFilter GROUP BY time($interval)');
+                          '$timeFilter GROUP BY time($interval)');
     });
   });
 
   describe('series with groupByTag', function() {
     it('should generate correct query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: 'cpu',
-      tags: [],
-      groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}],
+        measurement: 'cpu',
+        tags: [],
+        groupBy: [{type: 'time', interval: 'auto'}, {type: 'tag', key: 'host'}],
       });
 
       var query = builder.build();
       expect(query).to.be('SELECT mean("value") AS "value" FROM "cpu" WHERE $timeFilter ' +
-          'GROUP BY time($interval), "host"');
+                          'GROUP BY time($interval), "host"');
     });
   });
 
@@ -126,8 +126,7 @@ describe('InfluxQueryBuilder', function() {
 
     it('should handle regex measurement in tag keys query', function() {
       var builder = new InfluxQueryBuilder({
-      measurement: '/.*/',
-      tags: []
+        measurement: '/.*/', tags: []
       });
       var query = builder.buildExploreQuery('TAG_KEYS');
       expect(query).to.be('SHOW TAG KEYS FROM /.*/');
@@ -170,7 +169,10 @@ describe('InfluxQueryBuilder', function() {
     });
 
     it('should switch to regex operator in tag condition', function() {
-      var builder = new InfluxQueryBuilder({measurement: 'cpu', tags: [{key: 'host', value: '/server.*/'}]});
+      var builder = new InfluxQueryBuilder({
+        measurement: 'cpu',
+        tags: [{key: 'host', value: '/server.*/'}]
+      });
       var query = builder.buildExploreQuery('TAG_VALUES', 'app');
       expect(query).to.be('SHOW TAG VALUES FROM "cpu" WITH KEY = "app" WHERE "host" =~ /server.*/');
     });

+ 41 - 0
public/app/plugins/datasource/influxdb/specs/query_part_specs.ts

@@ -0,0 +1,41 @@
+
+import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
+
+import queryPart = require('../query_part');
+
+describe('InfluxQueryBuilder', () => {
+
+  describe('series with mesurement only', () => {
+    it('should handle nested function parts', () => {
+      var part = queryPart.create({
+        name: 'derivative',
+        params: ['10s'],
+      });
+
+      expect(part.text).to.be('derivative(10s)');
+      expect(part.render('mean(value)')).to.be('derivative(mean(value), 10s)');
+    });
+
+    it('should handle suffirx parts', () => {
+      var part = queryPart.create({
+        name: 'math',
+        params: ['/ 100'],
+      });
+
+      expect(part.text).to.be('math(/ 100)');
+      expect(part.render('mean(value)')).to.be('mean(value) / 100');
+    });
+
+    it('should handle alias parts', () => {
+      var part = queryPart.create({
+        name: 'alias',
+        params: ['test'],
+      });
+
+      expect(part.text).to.be('alias(test)');
+      expect(part.render('mean(value)')).to.be('mean(value) AS "test"');
+    });
+
+  });
+
+});