Browse Source

Merge remote-tracking branch 'origin/graphite-query-editor-enhancements'

Torkel Ödegaard 8 năm trước cách đây
mục cha
commit
12b08b61d7

+ 0 - 1
.editorconfig

@@ -8,7 +8,6 @@ charset = utf-8
 trim_trailing_whitespace = true
 trim_trailing_whitespace = true
 insert_final_newline = true
 insert_final_newline = true
 max_line_length = 120
 max_line_length = 120
-insert_final_newline = true
 
 
 [*.go]
 [*.go]
 indent_style = tab
 indent_style = tab

+ 2 - 1
package.json

@@ -134,7 +134,7 @@
     "clipboard": "^1.7.1",
     "clipboard": "^1.7.1",
     "d3": "^4.11.0",
     "d3": "^4.11.0",
     "d3-scale-chromatic": "^1.1.1",
     "d3-scale-chromatic": "^1.1.1",
-    "eventemitter3": "^2.0.2",
+    "eventemitter3": "^2.0.3",
     "file-saver": "^1.3.3",
     "file-saver": "^1.3.3",
     "jquery": "^3.2.1",
     "jquery": "^3.2.1",
     "lodash": "^4.17.4",
     "lodash": "^4.17.4",
@@ -153,6 +153,7 @@
     "react-select": "^1.1.0",
     "react-select": "^1.1.0",
     "react-sizeme": "^2.3.6",
     "react-sizeme": "^2.3.6",
     "remarkable": "^1.7.1",
     "remarkable": "^1.7.1",
+    "rst2html": "github:thoward/rst2html#990cb89",
     "rxjs": "^5.4.3",
     "rxjs": "^5.4.3",
     "tether": "^1.4.0",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop",
     "tether-drop": "https://github.com/torkelo/drop",

+ 22 - 9
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -1,9 +1,11 @@
 import _ from 'lodash';
 import _ from 'lodash';
-import $ from 'jquery';
 import coreModule from '../../core_module';
 import coreModule from '../../core_module';
 
 
 function typeaheadMatcher(item) {
 function typeaheadMatcher(item) {
   var str = this.query;
   var str = this.query;
+  if (str === '') {
+    return true;
+  }
   if (str[0] === '/') {
   if (str[0] === '/') {
     str = str.substring(1);
     str = str.substring(1);
   }
   }
@@ -30,6 +32,8 @@ export class FormDropdownCtrl {
   getOptions: any;
   getOptions: any;
   optionCache: any;
   optionCache: any;
   lookupText: boolean;
   lookupText: boolean;
+  placeholder: any;
+  startOpen: any;
 
 
   /** @ngInject **/
   /** @ngInject **/
   constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
   constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
@@ -47,6 +51,10 @@ export class FormDropdownCtrl {
       this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
       this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
     }
     }
 
 
+    if (this.placeholder) {
+      this.inputElement.attr('placeholder', this.placeholder);
+    }
+
     this.inputElement.attr('data-provide', 'typeahead');
     this.inputElement.attr('data-provide', 'typeahead');
     this.inputElement.typeahead({
     this.inputElement.typeahead({
       source: this.typeaheadSource.bind(this),
       source: this.typeaheadSource.bind(this),
@@ -61,8 +69,7 @@ export class FormDropdownCtrl {
     var typeahead = this.inputElement.data('typeahead');
     var typeahead = this.inputElement.data('typeahead');
     typeahead.lookup = function() {
     typeahead.lookup = function() {
       this.query = this.$element.val() || '';
       this.query = this.$element.val() || '';
-      var items = this.source(this.query, $.proxy(this.process, this));
-      return items ? this.process(items) : items;
+      this.source(this.query, this.process.bind(this));
     };
     };
 
 
     this.linkElement.keydown(evt => {
     this.linkElement.keydown(evt => {
@@ -81,6 +88,10 @@ export class FormDropdownCtrl {
     });
     });
 
 
     this.inputElement.blur(this.inputBlur.bind(this));
     this.inputElement.blur(this.inputBlur.bind(this));
+
+    if (this.startOpen) {
+      setTimeout(this.open.bind(this), 0);
+    }
   }
   }
 
 
   getOptionsInternal(query) {
   getOptionsInternal(query) {
@@ -121,9 +132,9 @@ export class FormDropdownCtrl {
       });
       });
 
 
       // add custom values
       // add custom values
-      if (this.allowCustom) {
+      if (this.allowCustom && this.text !== '') {
         if (_.indexOf(optionTexts, this.text) === -1) {
         if (_.indexOf(optionTexts, this.text) === -1) {
-          options.unshift(this.text);
+          optionTexts.unshift(this.text);
         }
         }
       }
       }
 
 
@@ -228,10 +239,10 @@ const template = `
   style="display:none">
   style="display:none">
 </input>
 </input>
 <a ng-class="ctrl.cssClasses"
 <a ng-class="ctrl.cssClasses"
-	 tabindex="1"
-	 ng-click="ctrl.open()"
-	 give-focus="ctrl.focus"
-	 ng-bind-html="ctrl.display">
+   tabindex="1"
+   ng-click="ctrl.open()"
+   give-focus="ctrl.focus"
+   ng-bind-html="ctrl.display || '&nbsp;'">
 </a>
 </a>
 `;
 `;
 
 
@@ -250,6 +261,8 @@ export function formDropdownDirective() {
       allowCustom: '@',
       allowCustom: '@',
       labelMode: '@',
       labelMode: '@',
       lookupText: '@',
       lookupText: '@',
+      placeholder: '@',
+      startOpen: '@',
     },
     },
   };
   };
 }
 }

+ 1 - 1
public/app/core/directives/dropdown_typeahead.js

@@ -12,7 +12,7 @@ function (_, $, coreModule) {
       ' class="gf-form-input input-medium tight-form-input"' +
       ' class="gf-form-input input-medium tight-form-input"' +
       ' spellcheck="false" style="display:none"></input>';
       ' spellcheck="false" style="display:none"></input>';
 
 
-    var buttonTemplate = '<a  class="gf-form-label tight-form-func dropdown-toggle"' +
+    var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' +
       ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
       ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
       ' data-placement="top"><i class="fa fa-plus"></i></a>';
       ' data-placement="top"><i class="fa fa-plus"></i></a>';
 
 

+ 0 - 4
public/app/core/services/segment_srv.js

@@ -106,10 +106,6 @@ function (angular, _, coreModule) {
       return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
       return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
     };
     };
 
 
-    this.newSelectTagValue = function() {
-      return new MetricSegment({value: 'select tag value', fake: true});
-    };
-
   });
   });
 
 
 });
 });

+ 4 - 4
public/app/features/templating/partials/editor.html

@@ -16,12 +16,12 @@
 					Add variable
 					Add variable
 				</a>
 				</a>
 				<div class="grafana-info-box">
 				<div class="grafana-info-box">
-					<h5>What does variables do?</h5>
-					<p>Variables enables more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
+					<h5>What do variables do?</h5>
+					<p>Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
 					in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
 					in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
 					the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 					the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 
 
-					Checkout the
+					Check out the
 					<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
 					<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
 						Templating documentation
 						Templating documentation
 					</a> for more information.
 					</a> for more information.
@@ -93,7 +93,7 @@
 			</div>
 			</div>
 
 
 			<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
 			<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
-				<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__' that's reserved for Grafanas global variables</span>
+				<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__', that's reserved for Grafana's global variables</span>
 			</div>
 			</div>
 
 
 			<div class="gf-form-inline">
 			<div class="gf-form-inline">

+ 96 - 54
public/app/plugins/datasource/graphite/add_graphite_func.js

@@ -1,46 +1,37 @@
-define([
-  'angular',
-  'lodash',
-  'jquery',
-  './gfunc',
-],
-function (angular, _, $, gfunc) {
+define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) {
   'use strict';
   'use strict';
 
 
-  gfunc = gfunc.default;
+  angular.module('grafana.directives').directive('graphiteAddFunc', function($compile) {
+    var inputTemplate =
+      '<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
 
 
-  angular
-    .module('grafana.directives')
-    .directive('graphiteAddFunc', function($compile) {
-      var inputTemplate = '<input type="text"'+
-                            ' class="gf-form-input"' +
-                            ' spellcheck="false" style="display:none"></input>';
+    var buttonTemplate =
+      '<a class="gf-form-label query-part dropdown-toggle"' +
+      ' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
+      '<i class="fa fa-plus"></i></a>';
 
 
-      var buttonTemplate = '<a  class="gf-form-label query-part dropdown-toggle"' +
-                              ' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
-                              '<i class="fa fa-plus"></i></a>';
+    return {
+      link: function($scope, elem) {
+        var ctrl = $scope.ctrl;
 
 
-      return {
-        link: function($scope, elem) {
-          var ctrl = $scope.ctrl;
-          var graphiteVersion = ctrl.datasource.graphiteVersion;
-          var categories = gfunc.getCategories(graphiteVersion);
-          var allFunctions = getAllFunctionNames(categories);
+        var $input = $(inputTemplate);
+        var $button = $(buttonTemplate);
 
 
-          $scope.functionMenu = createFunctionDropDownMenu(categories);
+        $input.appendTo(elem);
+        $button.appendTo(elem);
 
 
-          var $input = $(inputTemplate);
-          var $button = $(buttonTemplate);
-          $input.appendTo(elem);
-          $button.appendTo(elem);
+        ctrl.datasource.getFuncDefs().then(function(funcDefs) {
+          var allFunctions = _.map(funcDefs, 'name').sort();
+
+          $scope.functionMenu = createFunctionDropDownMenu(funcDefs);
 
 
           $input.attr('data-provide', 'typeahead');
           $input.attr('data-provide', 'typeahead');
           $input.typeahead({
           $input.typeahead({
             source: allFunctions,
             source: allFunctions,
             minLength: 1,
             minLength: 1,
             items: 10,
             items: 10,
-            updater: function (value) {
-              var funcDef = gfunc.getFuncDef(value);
+            updater: function(value) {
+              var funcDef = ctrl.datasource.getFuncDef(value);
               if (!funcDef) {
               if (!funcDef) {
                 // try find close match
                 // try find close match
                 value = value.toLowerCase();
                 value = value.toLowerCase();
@@ -48,7 +39,9 @@ function (angular, _, $, gfunc) {
                   return funcName.toLowerCase().indexOf(value) === 0;
                   return funcName.toLowerCase().indexOf(value) === 0;
                 });
                 });
 
 
-                if (!funcDef) { return; }
+                if (!funcDef) {
+                  return;
+                }
               }
               }
 
 
               $scope.$apply(function() {
               $scope.$apply(function() {
@@ -57,7 +50,7 @@ function (angular, _, $, gfunc) {
 
 
               $input.trigger('blur');
               $input.trigger('blur');
               return '';
               return '';
-            }
+            },
           });
           });
 
 
           $button.click(function() {
           $button.click(function() {
@@ -82,32 +75,81 @@ function (angular, _, $, gfunc) {
           });
           });
 
 
           $compile(elem.contents())($scope);
           $compile(elem.contents())($scope);
-        }
-      };
-    });
+        });
+
+        var drop;
+        var cleanUpDrop = function() {
+          if (drop) {
+            drop.destroy();
+            drop = null;
+          }
+        };
+
+        $(elem)
+          .on('mouseenter', 'ul.dropdown-menu li', function() {
+            cleanUpDrop();
+
+            var funcDef;
+            try {
+              funcDef = ctrl.datasource.getFuncDef($('a', this).text());
+            } catch (e) {
+              // ignore
+            }
+
+            if (funcDef && funcDef.description) {
+              var shortDesc = funcDef.description;
+              if (shortDesc.length > 500) {
+                shortDesc = shortDesc.substring(0, 497) + '...';
+              }
 
 
-  function getAllFunctionNames(categories) {
-    return _.reduce(categories, function(list, category) {
-      _.each(category, function(func) {
-        list.push(func.name);
+              var contentElement = document.createElement('div');
+              contentElement.innerHTML = '<h4>' + funcDef.name + '</h4>' + rst2html(shortDesc);
+
+              drop = new Drop({
+                target: this,
+                content: contentElement,
+                classes: 'drop-popover',
+                openOn: 'always',
+                tetherOptions: {
+                  attachment: 'bottom left',
+                  targetAttachment: 'bottom right',
+                },
+              });
+            }
+          })
+          .on('mouseout', 'ul.dropdown-menu li', function() {
+            cleanUpDrop();
+          });
+
+        $scope.$on('$destroy', cleanUpDrop);
+      },
+    };
+  });
+
+  function createFunctionDropDownMenu(funcDefs) {
+    var categories = {};
+
+    _.forEach(funcDefs, function(funcDef) {
+      if (!funcDef.category) {
+        return;
+      }
+      if (!categories[funcDef.category]) {
+        categories[funcDef.category] = [];
+      }
+      categories[funcDef.category].push({
+        text: funcDef.name,
+        click: "ctrl.addFunction('" + funcDef.name + "')",
       });
       });
-      return list;
-    }, []);
-  }
+    });
 
 
-  function createFunctionDropDownMenu(categories) {
-    return _.map(categories, function(list, category) {
-      var submenu = _.map(list, function(value) {
+    return _.sortBy(
+      _.map(categories, function(submenu, category) {
         return {
         return {
-          text: value.name,
-          click: "ctrl.addFunction('" + value.name + "')",
+          text: category,
+          submenu: _.sortBy(submenu, 'text'),
         };
         };
-      });
-
-      return {
-        text: category,
-        submenu: submenu
-      };
-    });
+      }),
+      'text'
+    );
   }
   }
 });
 });

+ 0 - 1
public/app/plugins/datasource/graphite/config_ctrl.ts

@@ -8,7 +8,6 @@ export class GraphiteConfigCtrl {
     this.datasourceSrv = datasourceSrv;
     this.datasourceSrv = datasourceSrv;
     this.current.jsonData = this.current.jsonData || {};
     this.current.jsonData = this.current.jsonData || {};
     this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
     this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
-
     this.autoDetectGraphiteVersion();
     this.autoDetectGraphiteVersion();
   }
   }
 
 

+ 118 - 11
public/app/plugins/datasource/graphite/datasource.ts

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import * as dateMath from 'app/core/utils/datemath';
 import * as dateMath from 'app/core/utils/datemath';
 import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
 import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
+import gfunc from './gfunc';
 
 
 /** @ngInject */
 /** @ngInject */
 export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
 export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
@@ -12,6 +13,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
   this.cacheTimeout = instanceSettings.cacheTimeout;
   this.cacheTimeout = instanceSettings.cacheTimeout;
   this.withCredentials = instanceSettings.withCredentials;
   this.withCredentials = instanceSettings.withCredentials;
   this.render_method = instanceSettings.render_method || 'POST';
   this.render_method = instanceSettings.render_method || 'POST';
+  this.funcDefs = null;
+  this.funcDefsPromise = null;
 
 
   this.getQueryOptionsInfo = function() {
   this.getQueryOptionsInfo = function() {
     return {
     return {
@@ -200,6 +203,35 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     let options = optionalOptions || {};
     let options = optionalOptions || {};
     let interpolatedQuery = templateSrv.replace(query);
     let interpolatedQuery = templateSrv.replace(query);
 
 
+    // special handling for tag_values(<tag>[,<expression>]*), this is used for template variables
+    let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/);
+    if (matches) {
+      const expressions = [];
+      const exprRegex = /, *([^,]+)/g;
+      let match;
+      while ((match = exprRegex.exec(matches[2])) !== null) {
+        expressions.push(match[1]);
+      }
+      options.limit = 10000;
+      return this.getTagValuesAutoComplete(expressions, matches[1], undefined, options);
+    }
+
+    // special handling for tags(<expression>[,<expression>]*), this is used for template variables
+    matches = interpolatedQuery.match(/^tags\(([^,]*)((, *[^,]+)*)\)$/);
+    if (matches) {
+      const expressions = [];
+      if (matches[1]) {
+        expressions.push(matches[1]);
+        const exprRegex = /, *([^,]+)/g;
+        let match;
+        while ((match = exprRegex.exec(matches[2])) !== null) {
+          expressions.push(match[1]);
+        }
+      }
+      options.limit = 10000;
+      return this.getTagsAutoComplete(expressions, undefined, options);
+    }
+
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
       url: '/metrics/find',
       url: '/metrics/find',
@@ -210,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       requestId: options.requestId,
       requestId: options.requestId,
     };
     };
 
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
     }
@@ -235,7 +267,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       requestId: options.requestId,
       requestId: options.requestId,
     };
     };
 
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
     }
@@ -255,12 +287,12 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
 
 
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
-      url: '/tags/' + tag,
+      url: '/tags/' + templateSrv.replace(tag),
       // for cancellations
       // for cancellations
       requestId: options.requestId,
       requestId: options.requestId,
     };
     };
 
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
     }
@@ -279,18 +311,29 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     });
     });
   };
   };
 
 
-  this.getTagsAutoComplete = (expression, tagPrefix) => {
+  this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => {
+    let options = optionalOptions || {};
+
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
       url: '/tags/autoComplete/tags',
       url: '/tags/autoComplete/tags',
       params: {
       params: {
-        expr: expression,
+        expr: _.map(expressions, expression => templateSrv.replace(expression)),
       },
       },
+      // for cancellations
+      requestId: options.requestId,
     };
     };
 
 
     if (tagPrefix) {
     if (tagPrefix) {
       httpOptions.params.tagPrefix = tagPrefix;
       httpOptions.params.tagPrefix = tagPrefix;
     }
     }
+    if (options.limit) {
+      httpOptions.params.limit = options.limit;
+    }
+    if (options.range) {
+      httpOptions.params.from = this.translateTime(options.range.from, false);
+      httpOptions.params.until = this.translateTime(options.range.to, true);
+    }
 
 
     return this.doGraphiteRequest(httpOptions).then(results => {
     return this.doGraphiteRequest(httpOptions).then(results => {
       if (results.data) {
       if (results.data) {
@@ -303,19 +346,30 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     });
     });
   };
   };
 
 
-  this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => {
+  this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => {
+    let options = optionalOptions || {};
+
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
       url: '/tags/autoComplete/values',
       url: '/tags/autoComplete/values',
       params: {
       params: {
-        expr: expression,
-        tag: tag,
+        expr: _.map(expressions, expression => templateSrv.replace(expression)),
+        tag: templateSrv.replace(tag),
       },
       },
+      // for cancellations
+      requestId: options.requestId,
     };
     };
 
 
     if (valuePrefix) {
     if (valuePrefix) {
       httpOptions.params.valuePrefix = valuePrefix;
       httpOptions.params.valuePrefix = valuePrefix;
     }
     }
+    if (options.limit) {
+      httpOptions.params.limit = options.limit;
+    }
+    if (options.range) {
+      httpOptions.params.from = this.translateTime(options.range.from, false);
+      httpOptions.params.until = this.translateTime(options.range.to, true);
+    }
 
 
     return this.doGraphiteRequest(httpOptions).then(results => {
     return this.doGraphiteRequest(httpOptions).then(results => {
       if (results.data) {
       if (results.data) {
@@ -328,10 +382,13 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     });
     });
   };
   };
 
 
-  this.getVersion = function() {
+  this.getVersion = function(optionalOptions) {
+    let options = optionalOptions || {};
+
     let httpOptions = {
     let httpOptions = {
       method: 'GET',
       method: 'GET',
-      url: '/version/_', // Prevent last / trimming
+      url: '/version',
+      requestId: options.requestId,
     };
     };
 
 
     return this.doGraphiteRequest(httpOptions)
     return this.doGraphiteRequest(httpOptions)
@@ -347,6 +404,52 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       });
       });
   };
   };
 
 
+  this.createFuncInstance = function(funcDef, options?) {
+    return gfunc.createFuncInstance(funcDef, options, this.funcDefs);
+  };
+
+  this.getFuncDef = function(name) {
+    return gfunc.getFuncDef(name, this.funcDefs);
+  };
+
+  this.waitForFuncDefsLoaded = function() {
+    return this.getFuncDefs();
+  };
+
+  this.getFuncDefs = function() {
+    if (this.funcDefsPromise !== null) {
+      return this.funcDefsPromise;
+    }
+
+    if (!supportsFunctionIndex(this.graphiteVersion)) {
+      this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
+      this.funcDefsPromise = Promise.resolve(this.funcDefs);
+      return this.funcDefsPromise;
+    }
+
+    let httpOptions = {
+      method: 'GET',
+      url: '/functions',
+    };
+
+    this.funcDefsPromise = this.doGraphiteRequest(httpOptions)
+      .then(results => {
+        if (results.status !== 200 || typeof results.data !== 'object') {
+          this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
+        } else {
+          this.funcDefs = gfunc.parseFuncDefs(results.data);
+        }
+        return this.funcDefs;
+      })
+      .catch(err => {
+        console.log('Fetching graphite functions error', err);
+        this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
+        return this.funcDefs;
+      });
+
+    return this.funcDefsPromise;
+  };
+
   this.testDatasource = function() {
   this.testDatasource = function() {
     return this.metricFindQuery('*').then(function() {
     return this.metricFindQuery('*').then(function() {
       return { status: 'success', message: 'Data source is working' };
       return { status: 'success', message: 'Data source is working' };
@@ -440,3 +543,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
 function supportsTags(version: string): boolean {
 function supportsTags(version: string): boolean {
   return isVersionGtOrEq(version, '1.1');
   return isVersionGtOrEq(version, '1.1');
 }
 }
+
+function supportsFunctionIndex(version: string): boolean {
+  return isVersionGtOrEq(version, '1.1');
+}

+ 95 - 33
public/app/plugins/datasource/graphite/func_editor.js

@@ -2,17 +2,18 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   'jquery',
   'jquery',
+  'rst2html',
 ],
 ],
-function (angular, _, $) {
+function (angular, _, $, rst2html) {
   'use strict';
   'use strict';
 
 
   angular
   angular
     .module('grafana.directives')
     .module('grafana.directives')
-    .directive('graphiteFuncEditor', function($compile, templateSrv) {
+    .directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) {
 
 
       var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
       var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
       var paramTemplate = '<input type="text" style="display:none"' +
       var paramTemplate = '<input type="text" style="display:none"' +
-                          ' class="input-mini tight-form-func-param"></input>';
+                          ' class="input-small tight-form-func-param"></input>';
 
 
       var funcControlsTemplate =
       var funcControlsTemplate =
          '<div class="tight-form-func-controls">' +
          '<div class="tight-form-func-controls">' +
@@ -29,19 +30,20 @@ function (angular, _, $) {
           var $funcControls = $(funcControlsTemplate);
           var $funcControls = $(funcControlsTemplate);
           var ctrl = $scope.ctrl;
           var ctrl = $scope.ctrl;
           var func = $scope.func;
           var func = $scope.func;
-          var funcDef = func.def;
           var scheduledRelink = false;
           var scheduledRelink = false;
           var paramCountAtLink = 0;
           var paramCountAtLink = 0;
+          var cancelBlur = null;
 
 
           function clickFuncParam(paramIndex) {
           function clickFuncParam(paramIndex) {
             /*jshint validthis:true */
             /*jshint validthis:true */
 
 
             var $link = $(this);
             var $link = $(this);
+            var $comma = $link.prev('.comma');
             var $input = $link.next();
             var $input = $link.next();
 
 
             $input.val(func.params[paramIndex]);
             $input.val(func.params[paramIndex]);
-            $input.css('width', ($link.width() + 16) + 'px');
 
 
+            $comma.removeClass('last');
             $link.hide();
             $link.hide();
             $input.show();
             $input.show();
             $input.focus();
             $input.focus();
@@ -68,31 +70,64 @@ function (angular, _, $) {
             }
             }
           }
           }
 
 
-          function inputBlur(paramIndex) {
+          function paramDef(index) {
+            if (index < func.def.params.length) {
+              return func.def.params[index];
+            }
+            if (_.last(func.def.params).multiple) {
+              return _.assign({}, _.last(func.def.params), {optional: true});
+            }
+            return {};
+          }
+
+          function switchToLink(inputElem, paramIndex) {
             /*jshint validthis:true */
             /*jshint validthis:true */
-            var $input = $(this);
+            var $input = $(inputElem);
+
+            clearTimeout(cancelBlur);
+            cancelBlur = null;
+
             var $link = $input.prev();
             var $link = $input.prev();
+            var $comma = $link.prev('.comma');
             var newValue = $input.val();
             var newValue = $input.val();
 
 
-            if (newValue !== '' || func.def.params[paramIndex].optional) {
-              $link.html(templateSrv.highlightVariablesAsHtml(newValue));
+            // remove optional empty params
+            if (newValue !== '' || paramDef(paramIndex).optional) {
+              func.updateParam(newValue, paramIndex);
+              $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
+            }
 
 
-              func.updateParam($input.val(), paramIndex);
-              scheduledRelinkIfNeeded();
+            scheduledRelinkIfNeeded();
 
 
-              $scope.$apply(function() {
-                ctrl.targetChanged();
-              });
+            $scope.$apply(function() {
+              ctrl.targetChanged();
+            });
 
 
-              $input.hide();
-              $link.show();
+            if ($link.hasClass('last') && newValue === '') {
+              $comma.addClass('last');
+            } else {
+              $link.removeClass('last');
             }
             }
+
+            $input.hide();
+            $link.show();
+          }
+
+          // this = input element
+          function inputBlur(paramIndex) {
+            /*jshint validthis:true */
+            var inputElem = this;
+            // happens long before the click event on the typeahead options
+            // need to have long delay because the blur
+            cancelBlur = setTimeout(function() {
+              switchToLink(inputElem, paramIndex);
+            }, 200);
           }
           }
 
 
           function inputKeyPress(paramIndex, e) {
           function inputKeyPress(paramIndex, e) {
             /*jshint validthis:true */
             /*jshint validthis:true */
             if(e.which === 13) {
             if(e.which === 13) {
-              inputBlur.call(this, paramIndex);
+              $(this).blur();
             }
             }
           }
           }
 
 
@@ -104,8 +139,8 @@ function (angular, _, $) {
           function addTypeahead($input, paramIndex) {
           function addTypeahead($input, paramIndex) {
             $input.attr('data-provide', 'typeahead');
             $input.attr('data-provide', 'typeahead');
 
 
-            var options = funcDef.params[paramIndex].options;
-            if (funcDef.params[paramIndex].type === 'int') {
+            var options = paramDef(paramIndex).options;
+            if (paramDef(paramIndex).type === 'int') {
               options = _.map(options, function(val) { return val.toString(); });
               options = _.map(options, function(val) { return val.toString(); });
             }
             }
 
 
@@ -114,9 +149,8 @@ function (angular, _, $) {
               minLength: 0,
               minLength: 0,
               items: 20,
               items: 20,
               updater: function (value) {
               updater: function (value) {
-                setTimeout(function() {
-                  inputBlur.call($input[0], paramIndex);
-                }, 0);
+                $input.val(value);
+                switchToLink($input[0], paramIndex);
                 return value;
                 return value;
               }
               }
             });
             });
@@ -148,18 +182,34 @@ function (angular, _, $) {
             $funcControls.appendTo(elem);
             $funcControls.appendTo(elem);
             $funcLink.appendTo(elem);
             $funcLink.appendTo(elem);
 
 
-            _.each(funcDef.params, function(param, index) {
-              if (param.optional && func.params.length <= index) {
-                return;
+            var defParams = _.clone(func.def.params);
+            var lastParam = _.last(func.def.params);
+
+            while (func.params.length >= defParams.length && lastParam && lastParam.multiple) {
+              defParams.push(_.assign({}, lastParam, {optional: true}));
+            }
+
+            _.each(defParams, function(param, index) {
+              if (param.optional && func.params.length < index) {
+                return false;
+              }
+
+              var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
+
+              var last = (index >= func.params.length - 1) && param.optional && !paramValue;
+              if (last && param.multiple) {
+                paramValue = '+';
               }
               }
 
 
               if (index > 0) {
               if (index > 0) {
-                $('<span>, </span>').appendTo(elem);
+                $('<span class="comma' + (last ? ' last' : '') + '">, </span>').appendTo(elem);
               }
               }
 
 
-              var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
-              var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
+              var $paramLink = $(
+                '<a ng-click="" class="graphite-func-param-link' + (last ? ' last' : '') + '">'
+                + (paramValue || '&nbsp;') + '</a>');
               var $input = $(paramTemplate);
               var $input = $(paramTemplate);
+              $input.attr('placeholder', param.name);
 
 
               paramCountAtLink++;
               paramCountAtLink++;
 
 
@@ -171,10 +221,9 @@ function (angular, _, $) {
               $input.keypress(_.partial(inputKeyPress, index));
               $input.keypress(_.partial(inputKeyPress, index));
               $paramLink.click(_.partial(clickFuncParam, index));
               $paramLink.click(_.partial(clickFuncParam, index));
 
 
-              if (funcDef.params[index].options) {
+              if (param.options) {
                 addTypeahead($input, index);
                 addTypeahead($input, index);
               }
               }
-
             });
             });
 
 
             $('<span>)</span>').appendTo(elem);
             $('<span>)</span>').appendTo(elem);
@@ -182,7 +231,7 @@ function (angular, _, $) {
             $compile(elem.contents())($scope);
             $compile(elem.contents())($scope);
           }
           }
 
 
-          function ifJustAddedFocusFistParam() {
+          function ifJustAddedFocusFirstParam() {
             if ($scope.func.added) {
             if ($scope.func.added) {
               $scope.func.added = false;
               $scope.func.added = false;
               setTimeout(function() {
               setTimeout(function() {
@@ -223,7 +272,20 @@ function (angular, _, $) {
               }
               }
 
 
               if ($target.hasClass('fa-question-circle')) {
               if ($target.hasClass('fa-question-circle')) {
-                window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank');
+                var funcDef = ctrl.datasource.getFuncDef(func.def.name);
+                if (funcDef && funcDef.description) {
+                  popoverSrv.show({
+                    element: e.target,
+                    position: 'bottom left',
+                    classNames: 'drop-popover drop-function-def',
+                    template: '<div style="overflow:auto;max-height:30rem;">'
+                      + '<h4>' + funcDef.name + '</h4>' + rst2html(funcDef.description) + '</div>',
+                    openOn: 'click',
+                  });
+                } else {
+                  window.open(
+                    "http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank');
+                }
                 return;
                 return;
               }
               }
             });
             });
@@ -233,7 +295,7 @@ function (angular, _, $) {
             elem.children().remove();
             elem.children().remove();
 
 
             addElementsAndCompile();
             addElementsAndCompile();
-            ifJustAddedFocusFistParam();
+            ifJustAddedFocusFirstParam();
             registerFuncControlsToggle();
             registerFuncControlsToggle();
             registerFuncControlsActions();
             registerFuncControlsActions();
           }
           }

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 121 - 217
public/app/plugins/datasource/graphite/gfunc.ts


+ 7 - 6
public/app/plugins/datasource/graphite/graphite_query.ts

@@ -1,8 +1,8 @@
 import _ from 'lodash';
 import _ from 'lodash';
-import gfunc from './gfunc';
 import { Parser } from './parser';
 import { Parser } from './parser';
 
 
 export default class GraphiteQuery {
 export default class GraphiteQuery {
+  datasource: any;
   target: any;
   target: any;
   functions: any[];
   functions: any[];
   segments: any[];
   segments: any[];
@@ -15,7 +15,8 @@ export default class GraphiteQuery {
   scopedVars: any;
   scopedVars: any;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(target, templateSrv?, scopedVars?) {
+  constructor(datasource, target, templateSrv?, scopedVars?) {
+    this.datasource = datasource;
     this.target = target;
     this.target = target;
     this.parseTarget();
     this.parseTarget();
 
 
@@ -86,7 +87,7 @@ export default class GraphiteQuery {
 
 
     switch (astNode.type) {
     switch (astNode.type) {
       case 'function':
       case 'function':
-        var innerFunc = gfunc.createFuncInstance(astNode.name, {
+        var innerFunc = this.datasource.createFuncInstance(astNode.name, {
           withDefaultParams: false,
           withDefaultParams: false,
         });
         });
         _.each(astNode.params, param => {
         _.each(astNode.params, param => {
@@ -133,7 +134,7 @@ export default class GraphiteQuery {
 
 
   moveAliasFuncLast() {
   moveAliasFuncLast() {
     var aliasFunc = _.find(this.functions, function(func) {
     var aliasFunc = _.find(this.functions, function(func) {
-      return func.def.name === 'alias' || func.def.name === 'aliasByNode' || func.def.name === 'aliasByMetric';
+      return func.def.name.startsWith('alias');
     });
     });
 
 
     if (aliasFunc) {
     if (aliasFunc) {
@@ -143,7 +144,7 @@ export default class GraphiteQuery {
   }
   }
 
 
   addFunctionParameter(func, value) {
   addFunctionParameter(func, value) {
-    if (func.params.length >= func.def.params.length) {
+    if (func.params.length >= func.def.params.length && !_.get(_.last(func.def.params), 'multiple', false)) {
       throw { message: 'too many parameters for function ' + func.def.name };
       throw { message: 'too many parameters for function ' + func.def.name };
     }
     }
     func.params.push(value);
     func.params.push(value);
@@ -208,7 +209,7 @@ export default class GraphiteQuery {
   }
   }
 
 
   splitSeriesByTagParams(func) {
   splitSeriesByTagParams(func) {
-    const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
+    const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/;
     return _.flatten(
     return _.flatten(
       _.map(func.params, (param: string) => {
       _.map(func.params, (param: string) => {
         let matches = tagPattern.exec(param);
         let matches = tagPattern.exec(param);

+ 35 - 15
public/app/plugins/datasource/graphite/partials/query.editor.html

@@ -10,30 +10,50 @@
         <label class="gf-form-label width-6 query-keyword">Series</label>
         <label class="gf-form-label width-6 query-keyword">Series</label>
       </div>
       </div>
 
 
-      <div ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
-        <gf-form-dropdown model="tag.key" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-key"
+      <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
+        <gf-form-dropdown
+          model="tag.key"
+          lookup-text="false"
+          allow-custom="true"
+          label-mode="true"
+          placeholder="Tag key"
+          css-class="query-segment-key"
           get-options="ctrl.getTags($index, $query)"
           get-options="ctrl.getTags($index, $query)"
-          on-change="ctrl.tagChanged(tag, $index)">
-        </gf-form-dropdown>
-        <gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator"
+          on-change="ctrl.tagChanged(tag, $index)"
+        />
+        <gf-form-dropdown
+          model="tag.operator"
+          lookup-text="false"
+          allow-custom="false"
+          label-mode="true"
+          css-class="query-segment-operator"
           get-options="ctrl.getTagOperators()"
           get-options="ctrl.getTagOperators()"
           on-change="ctrl.tagChanged(tag, $index)"
           on-change="ctrl.tagChanged(tag, $index)"
-					min-input-width="30">
-        </gf-form-dropdown>
-        <gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value"
+          min-input-width="30"
+        />
+        <gf-form-dropdown
+          model="tag.value"
+          lookup-text="false"
+          allow-custom="true"
+          label-mode="true"
+          css-class="query-segment-value"
+          placeholder="Tag value"
           get-options="ctrl.getTagValues(tag, $index, $query)"
           get-options="ctrl.getTagValues(tag, $index, $query)"
-          on-change="ctrl.tagChanged(tag, $index)">
-        </gf-form-dropdown>
+          on-change="ctrl.tagChanged(tag, $index)"
+        />
         <label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
         <label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
       </div>
       </div>
 
 
-      <div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
-        <metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
+      <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
+        <metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" />
+      </div>
+
+      <div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
+        <metric-segment segment="segment" get-options="ctrl.getAltSegments($index, $query)" on-change="ctrl.segmentValueChanged(segment, $index)" />
       </div>
       </div>
 
 
-      <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
-        <metric-segment segment="segment" get-options="ctrl.getTagsAsSegments()" on-change="ctrl.addNewTag(segment)">
-        </metric-segment>
+      <div ng-if="ctrl.paused" class="gf-form">
+        <a ng-click="ctrl.unpause()" class="gf-form-label query-part"><i class="fa fa-play"></i></a>
       </div>
       </div>
 
 
       <div class="gf-form gf-form--grow">
       <div class="gf-form gf-form--grow">

+ 33 - 20
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -2,7 +2,6 @@ import './add_graphite_func';
 import './func_editor';
 import './func_editor';
 
 
 import _ from 'lodash';
 import _ from 'lodash';
-import gfunc from './gfunc';
 import GraphiteQuery from './graphite_query';
 import GraphiteQuery from './graphite_query';
 import { QueryCtrl } from 'app/plugins/sdk';
 import { QueryCtrl } from 'app/plugins/sdk';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
@@ -18,17 +17,19 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   addTagSegments: any[];
   addTagSegments: any[];
   removeTagValue: string;
   removeTagValue: string;
   supportsTags: boolean;
   supportsTags: boolean;
+  paused: boolean;
 
 
   /** @ngInject **/
   /** @ngInject **/
-  constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
+  constructor($scope, $injector, private uiSegmentSrv, private templateSrv, $timeout) {
     super($scope, $injector);
     super($scope, $injector);
     this.supportsTags = this.datasource.supportsTags;
     this.supportsTags = this.datasource.supportsTags;
+    this.paused = false;
+    this.target.target = this.target.target || '';
 
 
-    if (this.target) {
-      this.target.target = this.target.target || '';
-      this.queryModel = new GraphiteQuery(this.target, templateSrv);
+    this.datasource.waitForFuncDefsLoaded().then(() => {
+      this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
       this.buildSegments();
       this.buildSegments();
-    }
+    });
 
 
     this.removeTagValue = '-- remove tag --';
     this.removeTagValue = '-- remove tag --';
   }
   }
@@ -104,8 +105,11 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     });
     });
   }
   }
 
 
-  getAltSegments(index) {
-    var query = index === 0 ? '*' : this.queryModel.getSegmentPathUpTo(index) + '.*';
+  getAltSegments(index, prefix) {
+    var query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*';
+    if (index > 0) {
+      query = this.queryModel.getSegmentPathUpTo(index) + '.' + query;
+    }
     var options = {
     var options = {
       range: this.panelCtrl.range,
       range: this.panelCtrl.range,
       requestId: 'get-alt-segments',
       requestId: 'get-alt-segments',
@@ -121,7 +125,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
           });
           });
         });
         });
 
 
-        if (altSegments.length === 0) {
+        if (index > 0 && altSegments.length === 0) {
           return altSegments;
           return altSegments;
         }
         }
 
 
@@ -158,7 +162,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
 
         if (this.supportsTags && index === 0) {
         if (this.supportsTags && index === 0) {
           this.removeTaggedEntry(altSegments);
           this.removeTaggedEntry(altSegments);
-          return this.addAltTagSegments(index, altSegments);
+          return this.addAltTagSegments(prefix, altSegments);
         } else {
         } else {
           return altSegments;
           return altSegments;
         }
         }
@@ -168,8 +172,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
       });
       });
   }
   }
 
 
-  addAltTagSegments(index, altSegments) {
-    return this.getTagsAsSegments().then(tagSegments => {
+  addAltTagSegments(prefix, altSegments) {
+    return this.getTagsAsSegments(prefix).then(tagSegments => {
       tagSegments = _.map(tagSegments, segment => {
       tagSegments = _.map(tagSegments, segment => {
         segment.value = TAG_PREFIX + segment.value;
         segment.value = TAG_PREFIX + segment.value;
         return segment;
         return segment;
@@ -192,6 +196,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
 
     if (segment.type === 'tag') {
     if (segment.type === 'tag') {
       let tag = removeTagPrefix(segment.value);
       let tag = removeTagPrefix(segment.value);
+      this.pause();
       this.addSeriesByTagFunc(tag);
       this.addSeriesByTagFunc(tag);
       return;
       return;
     }
     }
@@ -236,13 +241,13 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     var oldTarget = this.queryModel.target.target;
     var oldTarget = this.queryModel.target.target;
     this.updateModelTarget();
     this.updateModelTarget();
 
 
-    if (this.queryModel.target !== oldTarget) {
+    if (this.queryModel.target !== oldTarget && !this.paused) {
       this.panelCtrl.refresh();
       this.panelCtrl.refresh();
     }
     }
   }
   }
 
 
   addFunction(funcDef) {
   addFunction(funcDef) {
-    var newFunc = gfunc.createFuncInstance(funcDef, {
+    var newFunc = this.datasource.createFuncInstance(funcDef, {
       withDefaultParams: true,
       withDefaultParams: true,
     });
     });
     newFunc.added = true;
     newFunc.added = true;
@@ -268,11 +273,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   }
   }
 
 
   addSeriesByTagFunc(tag) {
   addSeriesByTagFunc(tag) {
-    let funcDef = gfunc.getFuncDef('seriesByTag');
-    let newFunc = gfunc.createFuncInstance(funcDef, {
+    let newFunc = this.datasource.createFuncInstance('seriesByTag', {
       withDefaultParams: false,
       withDefaultParams: false,
     });
     });
-    let tagParam = `${tag}=select tag value`;
+    let tagParam = `${tag}=`;
     newFunc.params = [tagParam];
     newFunc.params = [tagParam];
     this.queryModel.addFunction(newFunc);
     this.queryModel.addFunction(newFunc);
     newFunc.added = true;
     newFunc.added = true;
@@ -314,9 +318,9 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     });
     });
   }
   }
 
 
-  getTagsAsSegments() {
+  getTagsAsSegments(tagPrefix) {
     let tagExpressions = this.queryModel.renderTagExpressions();
     let tagExpressions = this.queryModel.renderTagExpressions();
-    return this.datasource.getTagsAutoComplete(tagExpressions).then(values => {
+    return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => {
       return _.map(values, val => {
       return _.map(values, val => {
         return this.uiSegmentSrv.newSegment({
         return this.uiSegmentSrv.newSegment({
           value: val.text,
           value: val.text,
@@ -355,7 +359,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
 
   addNewTag(segment) {
   addNewTag(segment) {
     let newTagKey = segment.value;
     let newTagKey = segment.value;
-    let newTag = { key: newTagKey, operator: '=', value: 'select tag value' };
+    let newTag = { key: newTagKey, operator: '=', value: '' };
     this.queryModel.addTag(newTag);
     this.queryModel.addTag(newTag);
     this.targetChanged();
     this.targetChanged();
     this.fixTagSegments();
     this.fixTagSegments();
@@ -374,6 +378,15 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   showDelimiter(index) {
   showDelimiter(index) {
     return index !== this.queryModel.tags.length - 1;
     return index !== this.queryModel.tags.length - 1;
   }
   }
+
+  pause() {
+    this.paused = true;
+  }
+
+  unpause() {
+    this.paused = false;
+    this.panelCtrl.refresh();
+  }
 }
 }
 
 
 function mapToDropdownOptions(results) {
 function mapToDropdownOptions(results) {

+ 6 - 5
public/app/plugins/datasource/graphite/specs/gfunc.jest.ts

@@ -5,7 +5,8 @@ describe('when creating func instance from func names', function() {
     var func = gfunc.createFuncInstance('sumSeries');
     var func = gfunc.createFuncInstance('sumSeries');
     expect(func).toBeTruthy();
     expect(func).toBeTruthy();
     expect(func.def.name).toEqual('sumSeries');
     expect(func.def.name).toEqual('sumSeries');
-    expect(func.def.params.length).toEqual(5);
+    expect(func.def.params.length).toEqual(1);
+    expect(func.def.params[0].multiple).toEqual(true);
     expect(func.def.defaultParams.length).toEqual(1);
     expect(func.def.defaultParams.length).toEqual(1);
   });
   });
 
 
@@ -74,10 +75,10 @@ describe('when rendering func instance', function() {
   });
   });
 });
 });
 
 
-describe('when requesting function categories', function() {
-  it('should return function categories', function() {
-    var catIndex = gfunc.getCategories('1.0');
-    expect(catIndex.Special.length).toBeGreaterThan(8);
+describe('when requesting function definitions', function() {
+  it('should return function definitions', function() {
+    var funcIndex = gfunc.getFuncDefs('1.0');
+    expect(Object.keys(funcIndex).length).toBeGreaterThan(8);
   });
   });
 });
 });
 
 

+ 20 - 6
public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts

@@ -24,6 +24,10 @@ describe('GraphiteQueryCtrl', function() {
       ctx.scope = $rootScope.$new();
       ctx.scope = $rootScope.$new();
       ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
       ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
       ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
       ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
+      ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0')));
+      ctx.datasource.getFuncDef = gfunc.getFuncDef;
+      ctx.datasource.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null));
+      ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
       ctx.panelCtrl = { panel: {} };
       ctx.panelCtrl = { panel: {} };
       ctx.panelCtrl = {
       ctx.panelCtrl = {
         panel: {
         panel: {
@@ -180,7 +184,21 @@ describe('GraphiteQueryCtrl', function() {
       ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
       ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
       ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
       ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
       ctx.ctrl.parseTarget();
       ctx.ctrl.parseTarget();
+    });
+
+    it('should add function params', function() {
+      expect(ctx.ctrl.queryModel.segments.length).to.be(1);
+      expect(ctx.ctrl.queryModel.segments[0].value).to.be('#A');
+
+      expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
+      expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(60);
+    });
+
+    it('target should remain the same', function() {
+      expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
+    });
 
 
+    it('targetFull should include nested queries', function() {
       ctx.ctrl.panelCtrl.panel.targets = [
       ctx.ctrl.panelCtrl.panel.targets = [
         {
         {
           target: 'nested.query.count',
           target: 'nested.query.count',
@@ -189,13 +207,9 @@ describe('GraphiteQueryCtrl', function() {
       ];
       ];
 
 
       ctx.ctrl.updateModelTarget();
       ctx.ctrl.updateModelTarget();
-    });
 
 
-    it('target should remain the same', function() {
       expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
       expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
-    });
 
 
-    it('targetFull should include nexted queries', function() {
       expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
       expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
     });
     });
   });
   });
@@ -271,12 +285,12 @@ describe('GraphiteQueryCtrl', function() {
     });
     });
 
 
     it('should update tags with default value', function() {
     it('should update tags with default value', function() {
-      const expected = [{ key: 'tag1', operator: '=', value: 'select tag value' }];
+      const expected = [{ key: 'tag1', operator: '=', value: '' }];
       expect(ctx.ctrl.queryModel.tags).to.eql(expected);
       expect(ctx.ctrl.queryModel.tags).to.eql(expected);
     });
     });
 
 
     it('should update target', function() {
     it('should update target', function() {
-      const expected = "seriesByTag('tag1=select tag value')";
+      const expected = "seriesByTag('tag1=')";
       expect(ctx.ctrl.target.target).to.eql(expected);
       expect(ctx.ctrl.target.target).to.eql(expected);
     });
     });
   });
   });

+ 33 - 27
public/sass/components/_query_editor.scss

@@ -89,7 +89,8 @@
   }
   }
 }
 }
 
 
-input[type="text"].tight-form-func-param {
+input[type='text'].tight-form-func-param {
+  font-size: 0.875rem;
   background: transparent;
   background: transparent;
   border: none;
   border: none;
   margin: 0;
   margin: 0;
@@ -129,32 +130,6 @@ input[type="text"].tight-form-func-param {
   }
   }
 }
 }
 
 
-input[type="text"].tight-form-func-param {
-  background: transparent;
-  border: none;
-  margin: 0;
-  padding: 0;
-}
-
-.tight-form-func-controls {
-  display: none;
-  text-align: center;
-
-  .fa-arrow-left {
-    float: left;
-    position: relative;
-    top: 2px;
-  }
-  .fa-arrow-right {
-    float: right;
-    position: relative;
-    top: 2px;
-  }
-  .fa-remove {
-    margin-left: 10px;
-  }
-}
-
 .query-troubleshooter {
 .query-troubleshooter {
   font-size: $font-size-sm;
   font-size: $font-size-sm;
   margin: $gf-form-margin;
   margin: $gf-form-margin;
@@ -176,3 +151,34 @@ input[type="text"].tight-form-func-param {
 .query-troubleshooter__body {
 .query-troubleshooter__body {
   padding: $spacer 0;
   padding: $spacer 0;
 }
 }
+
+.rst-text::before {
+  content: ' ';
+}
+
+.rst-unknown.rst-directive {
+  font-family: monospace;
+  margin-bottom: 1rem;
+}
+
+.rst-interpreted_text {
+  font-family: monospace;
+  display: inline;
+}
+
+.rst-bullet-list {
+  padding-left: 1.5rem;
+  margin-bottom: 1rem;
+}
+
+.rst-paragraph:last-child {
+  margin-bottom: 0;
+}
+
+.drop-element.drop-popover.drop-function-def .drop-content {
+  max-width: 30rem;
+}
+
+.rst-literal-block .rst-text {
+  display: block;
+}

+ 8 - 0
public/sass/components/_query_part.scss

@@ -6,4 +6,12 @@
     min-width: 100px;
     min-width: 100px;
     text-align: center;
     text-align: center;
   }
   }
+
+  .last {
+    display: none;
+  }
+
+  &:hover .last {
+    display: inline;
+  }
 }
 }

+ 2 - 0
scripts/grunt/default_task.js

@@ -19,6 +19,8 @@ module.exports = function(grunt) {
   ]);
   ]);
 
 
   grunt.registerTask('precommit', [
   grunt.registerTask('precommit', [
+    'jscs',
+    'jshint',
     'sasslint',
     'sasslint',
     'exec:tslint',
     'exec:tslint',
     'no-only-tests'
     'no-only-tests'

+ 180 - 3
yarn.lock

@@ -263,6 +263,10 @@ acorn-dynamic-import@^2.0.0:
   dependencies:
   dependencies:
     acorn "^4.0.3"
     acorn "^4.0.3"
 
 
+acorn-es7-plugin@^1.0.12:
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b"
+
 acorn-globals@^4.0.0:
 acorn-globals@^4.0.0:
   version "4.1.0"
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538"
@@ -279,7 +283,7 @@ acorn@^3.0.4:
   version "3.3.0"
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
 
 
-acorn@^4.0.3:
+acorn@^4.0.0, acorn@^4.0.3:
   version "4.0.13"
   version "4.0.13"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
 
 
@@ -525,6 +529,10 @@ array-equal@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
   resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
 
 
+array-filter@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+
 array-filter@~0.0.0:
 array-filter@~0.0.0:
   version "0.0.1"
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
   resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
@@ -1512,6 +1520,10 @@ call-limit@~1.1.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.0.tgz#6fd61b03f3da42a2cd0ec2b60f02bd0e71991fea"
   resolved "https://registry.yarnpkg.com/call-limit/-/call-limit-1.1.0.tgz#6fd61b03f3da42a2cd0ec2b60f02bd0e71991fea"
 
 
+call-signature@0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/call-signature/-/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996"
+
 caller-path@^0.1.0:
 caller-path@^0.1.0:
   version "0.1.0"
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
   resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -2072,6 +2084,10 @@ core-js@^1.0.0:
   version "1.2.7"
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
 
 
+core-js@^2.0.0:
+  version "2.5.3"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
+
 core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0:
 core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0:
   version "2.5.1"
   version "2.5.1"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b"
@@ -2744,6 +2760,10 @@ di@^0.0.1:
   version "0.0.1"
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
   resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
 
 
+diff-match-patch@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.0.tgz#1cc3c83a490d67f95d91e39f6ad1f2e086b63048"
+
 diff@3.3.1:
 diff@3.3.1:
   version "3.3.1"
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
@@ -2891,6 +2911,10 @@ each-async@^1.0.0:
     onetime "^1.0.0"
     onetime "^1.0.0"
     set-immediate-shim "^1.0.0"
     set-immediate-shim "^1.0.0"
 
 
+eastasianwidth@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.1.1.tgz#44d656de9da415694467335365fb3147b8572b7c"
+
 ecc-jsbn@~0.1.1:
 ecc-jsbn@~0.1.1:
   version "0.1.1"
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
@@ -2939,6 +2963,20 @@ emojis-list@^2.0.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
   resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
 
 
+empower-core@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-0.6.2.tgz#5adef566088e31fba80ba0a36df47d7094169144"
+  dependencies:
+    call-signature "0.0.2"
+    core-js "^2.0.0"
+
+empower@^1.2.3:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/empower/-/empower-1.2.3.tgz#6f0da73447f4edd838fec5c60313a88ba5cb852b"
+  dependencies:
+    core-js "^2.0.0"
+    empower-core "^0.6.2"
+
 encodeurl@~1.0.1:
 encodeurl@~1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
@@ -3266,6 +3304,12 @@ esprima@^4.0.0:
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
 
 
+espurify@^1.6.0:
+  version "1.7.0"
+  resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.7.0.tgz#1c5cf6cbccc32e6f639380bd4f991fab9ba9d226"
+  dependencies:
+    core-js "^2.0.0"
+
 esrecurse@^4.1.0:
 esrecurse@^4.1.0:
   version "4.2.0"
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
   resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
@@ -3300,7 +3344,7 @@ eventemitter3@1.x.x:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
 
 
-eventemitter3@^2.0.2:
+eventemitter3@^2.0.3:
   version "2.0.3"
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
 
 
@@ -7058,7 +7102,7 @@ object-is@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
   resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
 
 
-object-keys@^1.0.11, object-keys@^1.0.8:
+object-keys@^1.0.0, object-keys@^1.0.11, object-keys@^1.0.8:
   version "1.0.11"
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
 
 
@@ -7809,6 +7853,94 @@ postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.8:
     source-map "^0.6.1"
     source-map "^0.6.1"
     supports-color "^4.4.0"
     supports-color "^4.4.0"
 
 
+power-assert-context-formatter@^1.0.7:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz#edba352d3ed8a603114d667265acce60d689ccdf"
+  dependencies:
+    core-js "^2.0.0"
+    power-assert-context-traversal "^1.1.1"
+
+power-assert-context-reducer-ast@^1.0.7:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.1.2.tgz#484a99e26f4973ff8832e5c5cc756702e6094174"
+  dependencies:
+    acorn "^4.0.0"
+    acorn-es7-plugin "^1.0.12"
+    core-js "^2.0.0"
+    espurify "^1.6.0"
+    estraverse "^4.2.0"
+
+power-assert-context-traversal@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/power-assert-context-traversal/-/power-assert-context-traversal-1.1.1.tgz#88cabca0d13b6359f07d3d3e8afa699264577ed9"
+  dependencies:
+    core-js "^2.0.0"
+    estraverse "^4.1.0"
+
+power-assert-formatter@^1.3.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz#5dc125ed50a3dfb1dda26c19347f3bf58ec2884a"
+  dependencies:
+    core-js "^2.0.0"
+    power-assert-context-formatter "^1.0.7"
+    power-assert-context-reducer-ast "^1.0.7"
+    power-assert-renderer-assertion "^1.0.7"
+    power-assert-renderer-comparison "^1.0.7"
+    power-assert-renderer-diagram "^1.0.7"
+    power-assert-renderer-file "^1.0.7"
+
+power-assert-renderer-assertion@^1.0.7:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.1.1.tgz#cbfc0e77e0086a8f96af3f1d8e67b9ee7e28ce98"
+  dependencies:
+    power-assert-renderer-base "^1.1.1"
+    power-assert-util-string-width "^1.1.1"
+
+power-assert-renderer-base@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz#96a650c6fd05ee1bc1f66b54ad61442c8b3f63eb"
+
+power-assert-renderer-comparison@^1.0.7:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.1.1.tgz#d7439d97d85156be4e30a00f2fb5a72514ce3c08"
+  dependencies:
+    core-js "^2.0.0"
+    diff-match-patch "^1.0.0"
+    power-assert-renderer-base "^1.1.1"
+    stringifier "^1.3.0"
+    type-name "^2.0.1"
+
+power-assert-renderer-diagram@^1.0.7:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.1.2.tgz#655f8f711935a9b6d541b86327654717c637a986"
+  dependencies:
+    core-js "^2.0.0"
+    power-assert-renderer-base "^1.1.1"
+    power-assert-util-string-width "^1.1.1"
+    stringifier "^1.3.0"
+
+power-assert-renderer-file@^1.0.7:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/power-assert-renderer-file/-/power-assert-renderer-file-1.1.1.tgz#a37e2bbd178ccacd04e78dbb79c92fe34933c5e7"
+  dependencies:
+    power-assert-renderer-base "^1.1.1"
+
+power-assert-util-string-width@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/power-assert-util-string-width/-/power-assert-util-string-width-1.1.1.tgz#be659eb7937fdd2e6c9a77268daaf64bd5b7c592"
+  dependencies:
+    eastasianwidth "^0.1.1"
+
+power-assert@^1.2.0:
+  version "1.4.4"
+  resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.4.4.tgz#9295ea7437196f5a601fde420f042631186d7517"
+  dependencies:
+    define-properties "^1.1.2"
+    empower "^1.2.3"
+    power-assert-formatter "^1.3.1"
+    universal-deep-strict-equal "^1.2.1"
+    xtend "^4.0.0"
+
 prebuild-install@^2.3.0:
 prebuild-install@^2.3.0:
   version "2.3.0"
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.3.0.tgz#19481247df728b854ab57b187ce234211311b485"
   resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.3.0.tgz#19481247df728b854ab57b187ce234211311b485"
@@ -8651,6 +8783,15 @@ restore-cursor@^1.0.1:
     exit-hook "^1.0.0"
     exit-hook "^1.0.0"
     onetime "^1.0.0"
     onetime "^1.0.0"
 
 
+restructured@0.0.11:
+  version "0.0.11"
+  resolved "https://registry.yarnpkg.com/restructured/-/restructured-0.0.11.tgz#f914f6b6f358b8e45d6d8ee268926cf1a783f710"
+  dependencies:
+    commander "^2.9.0"
+    lodash "^4.0.0"
+    power-assert "^1.2.0"
+    unist-util-map "^1.0.2"
+
 ret@~0.1.10:
 ret@~0.1.10:
   version "0.1.15"
   version "0.1.15"
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
@@ -8693,6 +8834,12 @@ rst-selector-parser@^2.2.3:
     lodash.flattendeep "^4.4.0"
     lodash.flattendeep "^4.4.0"
     nearley "^2.7.10"
     nearley "^2.7.10"
 
 
+"rst2html@github:thoward/rst2html#990cb89":
+  version "1.0.4"
+  resolved "https://codeload.github.com/thoward/rst2html/tar.gz/990cb89f2a300cdd9151790be377c4c0840df809"
+  dependencies:
+    restructured "0.0.11"
+
 run-async@^0.1.0:
 run-async@^0.1.0:
   version "0.1.0"
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@@ -9359,6 +9506,14 @@ string_decoder@~0.10.x:
   version "0.10.31"
   version "0.10.31"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
 
 
+stringifier@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/stringifier/-/stringifier-1.3.0.tgz#def18342f6933db0f2dbfc9aa02175b448c17959"
+  dependencies:
+    core-js "^2.0.0"
+    traverse "^0.6.6"
+    type-name "^2.0.1"
+
 stringify-object@^3.2.0:
 stringify-object@^3.2.0:
   version "3.2.1"
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
   resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
@@ -9713,6 +9868,10 @@ tr46@^1.0.0:
   dependencies:
   dependencies:
     punycode "^2.1.0"
     punycode "^2.1.0"
 
 
+traverse@^0.6.6:
+  version "0.6.6"
+  resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+
 trim-newlines@^1.0.0:
 trim-newlines@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -9826,6 +9985,10 @@ type-is@~1.6.15:
     media-typer "0.3.0"
     media-typer "0.3.0"
     mime-types "~2.1.15"
     mime-types "~2.1.15"
 
 
+type-name@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/type-name/-/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4"
+
 typedarray@^0.0.6:
 typedarray@^0.0.6:
   version "0.0.6"
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -9952,6 +10115,20 @@ unique-string@^1.0.0:
   dependencies:
   dependencies:
     crypto-random-string "^1.0.0"
     crypto-random-string "^1.0.0"
 
 
+unist-util-map@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/unist-util-map/-/unist-util-map-1.0.3.tgz#26a913d7cddb3cd3e9a886d135d37a3d1f54e514"
+  dependencies:
+    object-assign "^4.0.1"
+
+universal-deep-strict-equal@^1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz#0da4ac2f73cff7924c81fa4de018ca562ca2b0a7"
+  dependencies:
+    array-filter "^1.0.0"
+    indexof "0.0.1"
+    object-keys "^1.0.0"
+
 universalify@^0.1.0:
 universalify@^0.1.0:
   version "0.1.1"
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác