Bladeren bron

poc: influxdb editor v3 test

Torkel Ödegaard 10 jaren geleden
bovenliggende
commit
e70b99a706

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

@@ -1,4 +1,4 @@
-<div class="tight-form-container-no-item-borders">
+<div class="">
 	<div  class="tight-form">
 	<div  class="tight-form">
 		<ul class="tight-form-list pull-right">
 		<ul class="tight-form-list pull-right">
 			<li ng-show="parserError" class="tight-form-item">
 			<li ng-show="parserError" class="tight-form-item">
@@ -48,26 +48,22 @@
 			<li>
 			<li>
 				<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
 				<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
 			</li>
 			</li>
+			<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
+				WHERE
+			</li>
+			<li ng-repeat="segment in tagSegments">
+				<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
+			</li>
 		</ul>
 		</ul>
 		<div class="clearfix"></div>
 		<div class="clearfix"></div>
 
 
 		<div style="padding: 10px" ng-if="target.rawQuery">
 		<div style="padding: 10px" ng-if="target.rawQuery">
 			<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
 			<textarea ng-model="target.query" rows="8" spellcheck="false" style="width: 100%; box-sizing: border-box;" ng-blur="get_data()"></textarea>
 		</div>
 		</div>
+
 	</div>
 	</div>
 
 
 	<div ng-hide="target.rawQuery">
 	<div ng-hide="target.rawQuery">
-		<div class="tight-form">
-			<ul class="tight-form-list">
-				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-					WHERE
-				</li>
-				<li ng-repeat="segment in tagSegments">
-					<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
 
 
 		<div class="tight-form" ng-repeat="field in target.fields">
 		<div class="tight-form" ng-repeat="field in target.fields">
 			<ul class="tight-form-list">
 			<ul class="tight-form-list">
@@ -75,19 +71,11 @@
 					<span ng-show="$index === 0">SELECT</span>
 					<span ng-show="$index === 0">SELECT</span>
 				</li>
 				</li>
 				<li>
 				<li>
-					<metric-segment-model property="field.func" get-options="getFunctions()" on-change="get_data()" css-class="tight-form-item-xlarge"></metric-segment>
+					<metric-segment-model property="field.name" get-options="getFields()" on-change="get_data()"></metric-segment>
 				</li>
 				</li>
-				<li>
-					<metric-segment-model property="field.name" get-options="getFields()" on-change="get_data()" css-class="tight-form-item-large"></metric-segment>
-				</li>
-				<li>
-					<input type="text" class="tight-form-clear-input text-center" style="width: 70px;" ng-model="field.mathExpr" spellcheck='false' placeholder="math expr" ng-blur="get_data()">
-				</li>
-				<li class="tight-form-item query-keyword">
-					AS
-				</li>
-				<li>
-					<input type="text" class="tight-form-clear-input" style="width: 180px;" ng-model="field.asExpr" spellcheck='false' placeholder="as expr" ng-blur="get_data()">
+				<li ng-repeat="func in field.parts">
+					<span influx-query-part-editor class="tight-form-item tight-form-func">
+					</span>
 				</li>
 				</li>
 			</ul>
 			</ul>
 
 
@@ -108,30 +96,29 @@
 					<span ng-show="$index === 0">GROUP BY</span>
 					<span ng-show="$index === 0">GROUP BY</span>
 				</li>
 				</li>
 				<li ng-if="groupBy.type === 'time'">
 				<li ng-if="groupBy.type === 'time'">
-					<span class="tight-form-item">time</span>
-					<metric-segment-model property="groupBy.interval" get-options="getGroupByTimeIntervals()" on-change="get_data()">
-					</metric-segment>
-				</li>
-				<li class="dropdown" ng-if="groupBy.type === 'time'">
-					<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">
-							fill ({{target.fill}})
-						</span>
-						<span ng-show="!target.fill">
-							no fill
-						</span>
-					</a>
-					<ul class="dropdown-menu">
-						<li><a ng-click="setFill('')">no fill</a></li>
-						<li><a ng-click="setFill('0')">fill (0)</a></li>
-						<li><a ng-click="setFill('null')">fill (null)</a></li>
-						<li><a ng-click="setFill('none')">fill (none)</a></li>
-						<li><a ng-click="setFill('previous')">fill (previous)</a></li>
-					</ul>
-				</li>
-				<li ng-if="groupBy.type === 'tag'">
-					<metric-segment-model property="groupBy.key" get-options="getTagOptions()" on-change="get_data()"></metric-segment>
-				</li>
+					<span influx-query-part-editor class="tight-form-item tight-form-func">
+					</span>
+				</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"> -->
+						<!-- 		<span ng&#45;show="target.fill"> -->
+							<!-- 			fill ({{target.fill}}) -->
+							<!-- 		</span> -->
+						<!-- 		<span ng&#45;show="!target.fill"> -->
+							<!-- 			no fill -->
+							<!-- 		</span> -->
+						<!-- 	</a> -->
+					<!-- 	<ul class="dropdown&#45;menu"> -->
+						<!-- 		<li><a ng&#45;click="setFill('')">no fill</a></li> -->
+						<!-- 		<li><a ng&#45;click="setFill('0')">fill (0)</a></li> -->
+						<!-- 		<li><a ng&#45;click="setFill('null')">fill (null)</a></li> -->
+						<!-- 		<li><a ng&#45;click="setFill('none')">fill (none)</a></li> -->
+						<!-- 		<li><a ng&#45;click="setFill('previous')">fill (previous)</a></li> -->
+						<!-- 	</ul> -->
+					<!-- </li> -->
+				<!-- <li ng&#45;if="groupBy.type === 'tag'"> -->
+					<!-- 	<metric&#45;segment&#45;model property="groupBy.key" get&#45;options="getTagOptions()" on&#45;change="get_data()"></metric&#45;segment> -->
+					<!-- </li> -->
 			</ul>
 			</ul>
 
 
 			<ul class="tight-form-list pull-right">
 			<ul class="tight-form-list pull-right">
@@ -146,6 +133,8 @@
 		</div>
 		</div>
 	</div>
 	</div>
 
 
+
+
 	<div class="tight-form">
 	<div class="tight-form">
 		<ul class="tight-form-list">
 		<ul class="tight-form-list">
 			<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
 			<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">

+ 11 - 1
public/app/plugins/datasource/influxdb/query_ctrl.js

@@ -2,8 +2,10 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   './query_builder',
   './query_builder',
+  './query_part',
+  './query_part_editor',
 ],
 ],
-function (angular, _, InfluxQueryBuilder) {
+function (angular, _, InfluxQueryBuilder, queryPart) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
@@ -17,6 +19,14 @@ function (angular, _, InfluxQueryBuilder) {
       target.tags = target.tags || [];
       target.tags = target.tags || [];
       target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
       target.groupBy = target.groupBy || [{type: 'time', interval: 'auto'}];
       target.fields = target.fields || [{name: 'value', func: target.function || 'mean'}];
       target.fields = target.fields || [{name: 'value', func: target.function || 'mean'}];
+      target.fields[0].parts = [
+        queryPart.create('mean', { withDefaultParams: true }),
+        queryPart.create('derivate', { withDefaultParams: true }),
+        queryPart.create('math', { withDefaultParams: true }),
+        queryPart.create('alias', { withDefaultParams: true }),
+      ];
+
+      $scope.func = queryPart.create('time', { withDefaultParams: true });
 
 
       $scope.queryBuilder = new InfluxQueryBuilder(target);
       $scope.queryBuilder = new InfluxQueryBuilder(target);
 
 

+ 161 - 0
public/app/plugins/datasource/influxdb/query_part.js

@@ -0,0 +1,161 @@
+define([
+  'lodash',
+  'jquery'
+],
+function (_, $) {
+  'use strict';
+
+  var index = [];
+  var categories = {
+    Combine: [],
+    Transform: [],
+    Calculate: [],
+    Filter: [],
+    Special: []
+  };
+
+  function addFuncDef(funcDef) {
+    funcDef.params = funcDef.params || [];
+    funcDef.defaultParams = funcDef.defaultParams || [];
+
+    if (funcDef.category) {
+      funcDef.category.push(funcDef);
+    }
+    index[funcDef.name] = funcDef;
+    index[funcDef.shortName || funcDef.name] = funcDef;
+  }
+
+  addFuncDef({
+    name: 'mean',
+    category: categories.Transform,
+    params: [],
+    defaultParams: [],
+  });
+
+  addFuncDef({
+    name: 'derivate',
+    category: categories.Transform,
+    params: [{ name: "rate", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
+    defaultParams: ['10s'],
+  });
+
+  addFuncDef({
+    name: 'time',
+    category: categories.Transform,
+    params: [{ name: "rate", type: "interval", options: ['$interval', '1s', '10s', '1m', '5min', '10m', '15m', '1h'] }],
+    defaultParams: ['$interval'],
+  });
+
+  addFuncDef({
+    name: 'math',
+    category: categories.Transform,
+    params: [{ name: "expr", type: "string"}],
+    defaultParams: [' / 100'],
+  });
+
+  addFuncDef({
+    name: 'alias',
+    category: categories.Transform,
+    params: [{ name: "name", type: "string"}],
+    defaultParams: ['alias'],
+  });
+
+  _.each(categories, function(funcList, catName) {
+    categories[catName] = _.sortBy(funcList, 'name');
+  });
+
+  function FuncInstance(funcDef, options) {
+    this.def = funcDef;
+    this.params = [];
+
+    if (options && options.withDefaultParams) {
+      this.params = funcDef.defaultParams.slice(0);
+    }
+
+    this.updateText();
+  }
+
+  FuncInstance.prototype.render = function(metricExp) {
+    var str = this.def.name + '(';
+    var parameters = _.map(this.params, function(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' && $.isNumeric(value)) {
+        return value;
+      }
+
+      return "'" + value + "'";
+
+    }, this);
+
+    if (metricExp) {
+      parameters.unshift(metricExp);
+    }
+
+    return str + parameters.join(', ') + ')';
+  };
+
+  FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
+    if (strValue.indexOf(',') === -1) {
+      return false;
+    }
+
+    return this.def.params[index + 1] && this.def.params[index + 1].optional;
+  };
+
+  FuncInstance.prototype.updateParam = function(strValue, index) {
+    // handle optional parameters
+    // if string contains ',' and next param is optional, split and update both
+    if (this._hasMultipleParamsInString(strValue, index)) {
+      _.each(strValue.split(','), function(partVal, idx) {
+        this.updateParam(partVal.trim(), idx);
+      }, this);
+      return;
+    }
+
+    if (strValue === '' && this.def.params[index].optional) {
+      this.params.splice(index, 1);
+    }
+    else {
+      this.params[index] = strValue;
+    }
+
+    this.updateText();
+  };
+
+  FuncInstance.prototype.updateText = function () {
+    if (this.params.length === 0) {
+      this.text = this.def.name + '()';
+      return;
+    }
+
+    var text = this.def.name + '(';
+    text += this.params.join(', ');
+    text += ')';
+    this.text = text;
+  };
+
+  return {
+    create: function(funcDef, options) {
+      if (_.isString(funcDef)) {
+        if (!index[funcDef]) {
+          throw { message: 'Method not found ' + name };
+        }
+        funcDef = index[funcDef];
+      }
+      return new FuncInstance(funcDef, options);
+    },
+
+    getFuncDef: function(name) {
+      return index[name];
+    },
+
+    getCategories: function() {
+      return categories;
+    }
+  };
+
+});

+ 244 - 0
public/app/plugins/datasource/influxdb/query_part_editor.js

@@ -0,0 +1,244 @@
+define([
+  'angular',
+  'lodash',
+  'jquery',
+],
+function (angular, _, $) {
+  'use strict';
+
+  angular
+    .module('grafana.directives')
+    .directive('influxQueryPartEditor', function($compile, templateSrv) {
+
+      var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
+      var paramTemplate = '<input type="text" style="display:none"' +
+                          ' class="input-mini tight-form-func-param"></input>';
+
+      var funcControlsTemplate =
+         '<div class="tight-form-func-controls">' +
+           '<span class="pointer fa fa-arrow-left"></span>' +
+           '<span class="pointer fa fa-question-circle"></span>' +
+           '<span class="pointer fa fa-remove" ></span>' +
+           '<span class="pointer fa fa-arrow-right"></span>' +
+         '</div>';
+
+      return {
+        restrict: 'A',
+        link: function postLink($scope, elem) {
+          var $funcLink = $(funcSpanTemplate);
+          var $funcControls = $(funcControlsTemplate);
+          var func = $scope.func;
+          var funcDef = func.def;
+          var scheduledRelink = false;
+          var paramCountAtLink = 0;
+
+          function clickFuncParam(paramIndex) {
+            /*jshint validthis:true */
+
+            var $link = $(this);
+            var $input = $link.next();
+
+            $input.val(func.params[paramIndex]);
+            $input.css('width', ($link.width() + 16) + 'px');
+
+            $link.hide();
+            $input.show();
+            $input.focus();
+            $input.select();
+
+            var typeahead = $input.data('typeahead');
+            if (typeahead) {
+              $input.val('');
+              typeahead.lookup();
+            }
+          }
+
+          function scheduledRelinkIfNeeded() {
+            if (paramCountAtLink === func.params.length) {
+              return;
+            }
+
+            if (!scheduledRelink) {
+              scheduledRelink = true;
+              setTimeout(function() {
+                relink();
+                scheduledRelink = false;
+              }, 200);
+            }
+          }
+
+          function inputBlur(paramIndex) {
+            /*jshint validthis:true */
+            var $input = $(this);
+            var $link = $input.prev();
+            var newValue = $input.val();
+
+            if (newValue !== '' || func.def.params[paramIndex].optional) {
+              $link.html(templateSrv.highlightVariablesAsHtml(newValue));
+
+              func.updateParam($input.val(), paramIndex);
+              scheduledRelinkIfNeeded();
+
+              $scope.$apply($scope.targetChanged);
+            }
+
+            $input.hide();
+            $link.show();
+          }
+
+          function inputKeyPress(paramIndex, e) {
+            /*jshint validthis:true */
+            if(e.which === 13) {
+              inputBlur.call(this, paramIndex);
+            }
+          }
+
+          function inputKeyDown() {
+            /*jshint validthis:true */
+            this.style.width = (3 + this.value.length) * 8 + 'px';
+          }
+
+          function addTypeahead($input, paramIndex) {
+            $input.attr('data-provide', 'typeahead');
+
+            var options = funcDef.params[paramIndex].options;
+            if (funcDef.params[paramIndex].type === 'int') {
+              options = _.map(options, function(val) { return val.toString(); });
+            }
+
+            $input.typeahead({
+              source: options,
+              minLength: 0,
+              items: 20,
+              updater: function (value) {
+                setTimeout(function() {
+                  inputBlur.call($input[0], paramIndex);
+                }, 0);
+                return value;
+              }
+            });
+
+            var typeahead = $input.data('typeahead');
+            typeahead.lookup = function () {
+              this.query = this.$element.val() || '';
+              return this.process(this.source);
+            };
+          }
+
+          function toggleFuncControls() {
+            var targetDiv = elem.closest('.tight-form');
+
+            if (elem.hasClass('show-function-controls')) {
+              elem.removeClass('show-function-controls');
+              targetDiv.removeClass('has-open-function');
+              $funcControls.hide();
+              return;
+            }
+
+            elem.addClass('show-function-controls');
+            targetDiv.addClass('has-open-function');
+
+            $funcControls.show();
+          }
+
+          function addElementsAndCompile() {
+            $funcControls.appendTo(elem);
+            $funcLink.appendTo(elem);
+
+            _.each(funcDef.params, function(param, index) {
+              if (param.optional && func.params.length <= index) {
+                return;
+              }
+
+              if (index > 0) {
+                $('<span>, </span>').appendTo(elem);
+              }
+
+              var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
+              var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
+              var $input = $(paramTemplate);
+
+              paramCountAtLink++;
+
+              $paramLink.appendTo(elem);
+              $input.appendTo(elem);
+
+              $input.blur(_.partial(inputBlur, index));
+              $input.keyup(inputKeyDown);
+              $input.keypress(_.partial(inputKeyPress, index));
+              $paramLink.click(_.partial(clickFuncParam, index));
+
+              if (funcDef.params[index].options) {
+                addTypeahead($input, index);
+              }
+
+            });
+
+            $('<span>)</span>').appendTo(elem);
+
+            $compile(elem.contents())($scope);
+          }
+
+          function ifJustAddedFocusFistParam() {
+            if ($scope.func.added) {
+              $scope.func.added = false;
+              setTimeout(function() {
+                elem.find('.graphite-func-param-link').first().click();
+              }, 10);
+            }
+          }
+
+          function registerFuncControlsToggle() {
+            $funcLink.click(toggleFuncControls);
+          }
+
+          function registerFuncControlsActions() {
+            $funcControls.click(function(e) {
+              var $target = $(e.target);
+              if ($target.hasClass('fa-remove')) {
+                toggleFuncControls();
+                $scope.$apply(function() {
+                  $scope.removeFunction($scope.func);
+                });
+                return;
+              }
+
+              if ($target.hasClass('fa-arrow-left')) {
+                $scope.$apply(function() {
+                  _.move($scope.functions, $scope.$index, $scope.$index - 1);
+                  $scope.targetChanged();
+                });
+                return;
+              }
+
+              if ($target.hasClass('fa-arrow-right')) {
+                $scope.$apply(function() {
+                  _.move($scope.functions, $scope.$index, $scope.$index + 1);
+                  $scope.targetChanged();
+                });
+                return;
+              }
+
+              if ($target.hasClass('fa-question-circle')) {
+                window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank');
+                return;
+              }
+            });
+          }
+
+          function relink() {
+            elem.children().remove();
+
+            addElementsAndCompile();
+            ifJustAddedFocusFistParam();
+            registerFuncControlsToggle();
+            registerFuncControlsActions();
+          }
+
+          relink();
+        }
+      };
+
+    });
+
+});