Sven Klemm 7 лет назад
Родитель
Сommit
181dfdba04

+ 118 - 0
public/app/core/components/sql_part/sql_part.ts

@@ -0,0 +1,118 @@
+import _ from 'lodash';
+
+export class SqlPartDef {
+  type: string;
+  params: any[];
+  defaultParams: any[];
+  renderer: any;
+  category: any;
+  addStrategy: any;
+
+  constructor(options: any) {
+    this.type = options.type;
+    this.params = options.params;
+    this.defaultParams = options.defaultParams;
+    this.renderer = options.renderer;
+    this.category = options.category;
+    this.addStrategy = options.addStrategy;
+  }
+}
+
+export class SqlPart {
+  part: any;
+  def: SqlPartDef;
+  params: any[];
+  text: string;
+
+  constructor(part: any, def: any) {
+    this.part = part;
+    this.def = def;
+    if (!this.def) {
+      throw { message: 'Could not find query part ' + part.type };
+    }
+
+    part.params = part.params || _.clone(this.def.defaultParams);
+    this.params = part.params;
+    this.updateText();
+  }
+
+  render(innerExpr: string) {
+    return this.def.renderer(this, innerExpr);
+  }
+
+  hasMultipleParamsInString(strValue, index) {
+    if (strValue.indexOf(',') === -1) {
+      return false;
+    }
+
+    return this.def.params[index + 1] && this.def.params[index + 1].optional;
+  }
+
+  updateParam(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(','), (partVal, idx) => {
+        this.updateParam(partVal.trim(), idx);
+      });
+      return;
+    }
+
+    if (strValue === '' && this.def.params[index].optional) {
+      this.params.splice(index, 1);
+    } else {
+      this.params[index] = strValue;
+    }
+
+    this.part.params = this.params;
+    this.updateText();
+  }
+
+  updateText() {
+    if (this.params.length === 0) {
+      this.text = this.def.type + '()';
+      return;
+    }
+
+    var text = this.def.type + '(';
+    text += this.params.join(', ');
+    text += ')';
+    this.text = text;
+  }
+}
+
+export function functionRenderer(part, innerExpr) {
+  var str = part.def.type + '(';
+  var parameters = _.map(part.params, (value, index) => {
+    var paramType = part.def.params[index];
+    if (paramType.type === 'time') {
+      if (value === 'auto') {
+        value = '$__interval';
+      }
+    }
+    if (paramType.quote === 'single') {
+      return "'" + value + "'";
+    } else if (paramType.quote === 'double') {
+      return '"' + value + '"';
+    }
+
+    return value;
+  });
+
+  if (innerExpr) {
+    parameters.unshift(innerExpr);
+  }
+  return str + parameters.join(', ') + ')';
+}
+
+export function suffixRenderer(part, innerExpr) {
+  return innerExpr + ' ' + part.params[0];
+}
+
+export function identityRenderer(part, innerExpr) {
+  return part.params[0];
+}
+
+export function quotedIdentityRenderer(part, innerExpr) {
+  return '"' + part.params[0] + '"';
+}

+ 185 - 0
public/app/core/components/sql_part/sql_part_editor.ts

@@ -0,0 +1,185 @@
+import _ from 'lodash';
+import $ from 'jquery';
+import coreModule from 'app/core/core_module';
+
+var template = `
+<div class="dropdown cascade-open">
+<a ng-click="showActionsMenu()" class="query-part-name pointer dropdown-toggle" data-toggle="dropdown">{{part.def.type}}</a>
+<span>(</span><span class="query-part-parameters"></span><span>)</span>
+<ul class="dropdown-menu">
+  <li ng-repeat="action in partActions">
+    <a ng-click="triggerPartAction(action)">{{action.text}}</a>
+  </li>
+</ul>
+`;
+
+/** @ngInject */
+export function sqlPartEditorDirective($compile, templateSrv) {
+  var paramTemplate = '<input type="text" class="hide input-mini tight-form-func-param"></input>';
+
+  return {
+    restrict: 'E',
+    template: template,
+    scope: {
+      part: '=',
+      handleEvent: '&',
+      debounce: '@',
+    },
+    link: function postLink($scope, elem) {
+      var part = $scope.part;
+      var partDef = part.def;
+      var $paramsContainer = elem.find('.query-part-parameters');
+      var debounceLookup = $scope.debounce;
+
+      $scope.partActions = [];
+
+      function clickFuncParam(paramIndex) {
+        /*jshint validthis:true */
+        var $link = $(this);
+        var $input = $link.next();
+
+        $input.val(part.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 inputBlur(paramIndex) {
+        /*jshint validthis:true */
+        var $input = $(this);
+        var $link = $input.prev();
+        var newValue = $input.val();
+
+        if (newValue !== '' || part.def.params[paramIndex].optional) {
+          $link.html(templateSrv.highlightVariablesAsHtml(newValue));
+
+          part.updateParam($input.val(), paramIndex);
+          $scope.$apply(() => {
+            $scope.handleEvent({ $event: { name: 'part-param-changed' } });
+          });
+        }
+
+        $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, param, paramIndex) {
+        if (!param.options && !param.dynamicLookup) {
+          return;
+        }
+
+        var typeaheadSource = function(query, callback) {
+          if (param.options) {
+            var options = param.options;
+            if (param.type === 'int') {
+              options = _.map(options, function(val) {
+                return val.toString();
+              });
+            }
+            return options;
+          }
+
+          $scope.$apply(function() {
+            $scope.handleEvent({ $event: { name: 'get-param-options' } }).then(function(result) {
+              var dynamicOptions = _.map(result, function(op) {
+                return op.value;
+              });
+              callback(dynamicOptions);
+            });
+          });
+        };
+
+        $input.attr('data-provide', 'typeahead');
+
+        $input.typeahead({
+          source: typeaheadSource,
+          minLength: 0,
+          items: 1000,
+          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() || '';
+          var items = this.source(this.query, $.proxy(this.process, this));
+          return items ? this.process(items) : items;
+        };
+
+        if (debounceLookup) {
+          typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
+        }
+      }
+
+      $scope.showActionsMenu = function() {
+        $scope.handleEvent({ $event: { name: 'get-part-actions' } }).then(res => {
+          $scope.partActions = res;
+        });
+      };
+
+      $scope.triggerPartAction = function(action) {
+        $scope.handleEvent({ $event: { name: 'action', action: action } });
+      };
+
+      function addElementsAndCompile() {
+        _.each(partDef.params, function(param, index) {
+          if (param.optional && part.params.length <= index) {
+            return;
+          }
+
+          if (index > 0) {
+            $('<span>, </span>').appendTo($paramsContainer);
+          }
+
+          var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
+          var $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
+          var $input = $(paramTemplate);
+
+          $paramLink.appendTo($paramsContainer);
+          $input.appendTo($paramsContainer);
+
+          $input.blur(_.partial(inputBlur, index));
+          $input.keyup(inputKeyDown);
+          $input.keypress(_.partial(inputKeyPress, index));
+          $paramLink.click(_.partial(clickFuncParam, index));
+
+          addTypeahead($input, param, index);
+        });
+      }
+
+      function relink() {
+        $paramsContainer.empty();
+        addElementsAndCompile();
+      }
+
+      relink();
+    },
+  };
+}
+
+coreModule.directive('sqlPartEditor', sqlPartEditorDirective);

+ 2 - 0
public/app/core/core.ts

@@ -31,6 +31,7 @@ import { layoutSelector } from './components/layout_selector/layout_selector';
 import { switchDirective } from './components/switch';
 import { switchDirective } from './components/switch';
 import { dashboardSelector } from './components/dashboard_selector';
 import { dashboardSelector } from './components/dashboard_selector';
 import { queryPartEditorDirective } from './components/query_part/query_part_editor';
 import { queryPartEditorDirective } from './components/query_part/query_part_editor';
+import { sqlPartEditorDirective } from './components/sql_part/sql_part_editor';
 import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
 import { formDropdownDirective } from './components/form_dropdown/form_dropdown';
 import 'app/core/controllers/all';
 import 'app/core/controllers/all';
 import 'app/core/services/all';
 import 'app/core/services/all';
@@ -74,6 +75,7 @@ export {
   appEvents,
   appEvents,
   dashboardSelector,
   dashboardSelector,
   queryPartEditorDirective,
   queryPartEditorDirective,
+  sqlPartEditorDirective,
   colors,
   colors,
   formDropdownDirective,
   formDropdownDirective,
   assignModelProperties,
   assignModelProperties,