瀏覽代碼

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

Torkel Ödegaard 8 年之前
父節點
當前提交
12b08b61d7

+ 0 - 1
.editorconfig

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

+ 2 - 1
package.json

@@ -134,7 +134,7 @@
     "clipboard": "^1.7.1",
     "d3": "^4.11.0",
     "d3-scale-chromatic": "^1.1.1",
-    "eventemitter3": "^2.0.2",
+    "eventemitter3": "^2.0.3",
     "file-saver": "^1.3.3",
     "jquery": "^3.2.1",
     "lodash": "^4.17.4",
@@ -153,6 +153,7 @@
     "react-select": "^1.1.0",
     "react-sizeme": "^2.3.6",
     "remarkable": "^1.7.1",
+    "rst2html": "github:thoward/rst2html#990cb89",
     "rxjs": "^5.4.3",
     "tether": "^1.4.0",
     "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 'jquery';
 import coreModule from '../../core_module';
 
 function typeaheadMatcher(item) {
   var str = this.query;
+  if (str === '') {
+    return true;
+  }
   if (str[0] === '/') {
     str = str.substring(1);
   }
@@ -30,6 +32,8 @@ export class FormDropdownCtrl {
   getOptions: any;
   optionCache: any;
   lookupText: boolean;
+  placeholder: any;
+  startOpen: any;
 
   /** @ngInject **/
   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;
     }
 
+    if (this.placeholder) {
+      this.inputElement.attr('placeholder', this.placeholder);
+    }
+
     this.inputElement.attr('data-provide', 'typeahead');
     this.inputElement.typeahead({
       source: this.typeaheadSource.bind(this),
@@ -61,8 +69,7 @@ export class FormDropdownCtrl {
     var typeahead = this.inputElement.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;
+      this.source(this.query, this.process.bind(this));
     };
 
     this.linkElement.keydown(evt => {
@@ -81,6 +88,10 @@ export class FormDropdownCtrl {
     });
 
     this.inputElement.blur(this.inputBlur.bind(this));
+
+    if (this.startOpen) {
+      setTimeout(this.open.bind(this), 0);
+    }
   }
 
   getOptionsInternal(query) {
@@ -121,9 +132,9 @@ export class FormDropdownCtrl {
       });
 
       // add custom values
-      if (this.allowCustom) {
+      if (this.allowCustom && this.text !== '') {
         if (_.indexOf(optionTexts, this.text) === -1) {
-          options.unshift(this.text);
+          optionTexts.unshift(this.text);
         }
       }
 
@@ -228,10 +239,10 @@ const template = `
   style="display:none">
 </input>
 <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>
 `;
 
@@ -250,6 +261,8 @@ export function formDropdownDirective() {
       allowCustom: '@',
       labelMode: '@',
       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"' +
       ' 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"' +
       ' 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' });
     };
 
-    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
 				</a>
 				<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
 					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">
 						Templating documentation
 					</a> for more information.
@@ -93,7 +93,7 @@
 			</div>
 
 			<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 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';
 
-  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.typeahead({
             source: allFunctions,
             minLength: 1,
             items: 10,
-            updater: function (value) {
-              var funcDef = gfunc.getFuncDef(value);
+            updater: function(value) {
+              var funcDef = ctrl.datasource.getFuncDef(value);
               if (!funcDef) {
                 // try find close match
                 value = value.toLowerCase();
@@ -48,7 +39,9 @@ function (angular, _, $, gfunc) {
                   return funcName.toLowerCase().indexOf(value) === 0;
                 });
 
-                if (!funcDef) { return; }
+                if (!funcDef) {
+                  return;
+                }
               }
 
               $scope.$apply(function() {
@@ -57,7 +50,7 @@ function (angular, _, $, gfunc) {
 
               $input.trigger('blur');
               return '';
-            }
+            },
           });
 
           $button.click(function() {
@@ -82,32 +75,81 @@ function (angular, _, $, gfunc) {
           });
 
           $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 {
-          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.current.jsonData = this.current.jsonData || {};
     this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
-
     this.autoDetectGraphiteVersion();
   }
 

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

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import * as dateMath from 'app/core/utils/datemath';
 import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
+import gfunc from './gfunc';
 
 /** @ngInject */
 export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
@@ -12,6 +13,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
   this.cacheTimeout = instanceSettings.cacheTimeout;
   this.withCredentials = instanceSettings.withCredentials;
   this.render_method = instanceSettings.render_method || 'POST';
+  this.funcDefs = null;
+  this.funcDefsPromise = null;
 
   this.getQueryOptionsInfo = function() {
     return {
@@ -200,6 +203,35 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     let options = optionalOptions || {};
     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 = {
       method: 'GET',
       url: '/metrics/find',
@@ -210,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       requestId: options.requestId,
     };
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
@@ -235,7 +267,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       requestId: options.requestId,
     };
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
@@ -255,12 +287,12 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
 
     let httpOptions: any = {
       method: 'GET',
-      url: '/tags/' + tag,
+      url: '/tags/' + templateSrv.replace(tag),
       // for cancellations
       requestId: options.requestId,
     };
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       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 = {
       method: 'GET',
       url: '/tags/autoComplete/tags',
       params: {
-        expr: expression,
+        expr: _.map(expressions, expression => templateSrv.replace(expression)),
       },
+      // for cancellations
+      requestId: options.requestId,
     };
 
     if (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 => {
       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 = {
       method: 'GET',
       url: '/tags/autoComplete/values',
       params: {
-        expr: expression,
-        tag: tag,
+        expr: _.map(expressions, expression => templateSrv.replace(expression)),
+        tag: templateSrv.replace(tag),
       },
+      // for cancellations
+      requestId: options.requestId,
     };
 
     if (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 => {
       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 = {
       method: 'GET',
-      url: '/version/_', // Prevent last / trimming
+      url: '/version',
+      requestId: options.requestId,
     };
 
     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() {
     return this.metricFindQuery('*').then(function() {
       return { status: 'success', message: 'Data source is working' };
@@ -440,3 +543,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
 function supportsTags(version: string): boolean {
   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',
   'lodash',
   'jquery',
+  'rst2html',
 ],
-function (angular, _, $) {
+function (angular, _, $, rst2html) {
   'use strict';
 
   angular
     .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 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 =
          '<div class="tight-form-func-controls">' +
@@ -29,19 +30,20 @@ function (angular, _, $) {
           var $funcControls = $(funcControlsTemplate);
           var ctrl = $scope.ctrl;
           var func = $scope.func;
-          var funcDef = func.def;
           var scheduledRelink = false;
           var paramCountAtLink = 0;
+          var cancelBlur = null;
 
           function clickFuncParam(paramIndex) {
             /*jshint validthis:true */
 
             var $link = $(this);
+            var $comma = $link.prev('.comma');
             var $input = $link.next();
 
             $input.val(func.params[paramIndex]);
-            $input.css('width', ($link.width() + 16) + 'px');
 
+            $comma.removeClass('last');
             $link.hide();
             $input.show();
             $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 */
-            var $input = $(this);
+            var $input = $(inputElem);
+
+            clearTimeout(cancelBlur);
+            cancelBlur = null;
+
             var $link = $input.prev();
+            var $comma = $link.prev('.comma');
             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) {
             /*jshint validthis:true */
             if(e.which === 13) {
-              inputBlur.call(this, paramIndex);
+              $(this).blur();
             }
           }
 
@@ -104,8 +139,8 @@ function (angular, _, $) {
           function addTypeahead($input, paramIndex) {
             $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(); });
             }
 
@@ -114,9 +149,8 @@ function (angular, _, $) {
               minLength: 0,
               items: 20,
               updater: function (value) {
-                setTimeout(function() {
-                  inputBlur.call($input[0], paramIndex);
-                }, 0);
+                $input.val(value);
+                switchToLink($input[0], paramIndex);
                 return value;
               }
             });
@@ -148,18 +182,34 @@ function (angular, _, $) {
             $funcControls.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) {
-                $('<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);
+              $input.attr('placeholder', param.name);
 
               paramCountAtLink++;
 
@@ -171,10 +221,9 @@ function (angular, _, $) {
               $input.keypress(_.partial(inputKeyPress, index));
               $paramLink.click(_.partial(clickFuncParam, index));
 
-              if (funcDef.params[index].options) {
+              if (param.options) {
                 addTypeahead($input, index);
               }
-
             });
 
             $('<span>)</span>').appendTo(elem);
@@ -182,7 +231,7 @@ function (angular, _, $) {
             $compile(elem.contents())($scope);
           }
 
-          function ifJustAddedFocusFistParam() {
+          function ifJustAddedFocusFirstParam() {
             if ($scope.func.added) {
               $scope.func.added = false;
               setTimeout(function() {
@@ -223,7 +272,20 @@ function (angular, _, $) {
               }
 
               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;
               }
             });
@@ -233,7 +295,7 @@ function (angular, _, $) {
             elem.children().remove();
 
             addElementsAndCompile();
-            ifJustAddedFocusFistParam();
+            ifJustAddedFocusFirstParam();
             registerFuncControlsToggle();
             registerFuncControlsActions();
           }

File diff suppressed because it is too large
+ 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 gfunc from './gfunc';
 import { Parser } from './parser';
 
 export default class GraphiteQuery {
+  datasource: any;
   target: any;
   functions: any[];
   segments: any[];
@@ -15,7 +15,8 @@ export default class GraphiteQuery {
   scopedVars: any;
 
   /** @ngInject */
-  constructor(target, templateSrv?, scopedVars?) {
+  constructor(datasource, target, templateSrv?, scopedVars?) {
+    this.datasource = datasource;
     this.target = target;
     this.parseTarget();
 
@@ -86,7 +87,7 @@ export default class GraphiteQuery {
 
     switch (astNode.type) {
       case 'function':
-        var innerFunc = gfunc.createFuncInstance(astNode.name, {
+        var innerFunc = this.datasource.createFuncInstance(astNode.name, {
           withDefaultParams: false,
         });
         _.each(astNode.params, param => {
@@ -133,7 +134,7 @@ export default class GraphiteQuery {
 
   moveAliasFuncLast() {
     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) {
@@ -143,7 +144,7 @@ export default class GraphiteQuery {
   }
 
   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 };
     }
     func.params.push(value);
@@ -208,7 +209,7 @@ export default class GraphiteQuery {
   }
 
   splitSeriesByTagParams(func) {
-    const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
+    const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/;
     return _.flatten(
       _.map(func.params, (param: string) => {
         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>
       </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)"
-          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()"
           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)"
-          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>
       </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 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 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 _ from 'lodash';
-import gfunc from './gfunc';
 import GraphiteQuery from './graphite_query';
 import { QueryCtrl } from 'app/plugins/sdk';
 import appEvents from 'app/core/app_events';
@@ -18,17 +17,19 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   addTagSegments: any[];
   removeTagValue: string;
   supportsTags: boolean;
+  paused: boolean;
 
   /** @ngInject **/
-  constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
+  constructor($scope, $injector, private uiSegmentSrv, private templateSrv, $timeout) {
     super($scope, $injector);
     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.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 = {
       range: this.panelCtrl.range,
       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;
         }
 
@@ -158,7 +162,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
         if (this.supportsTags && index === 0) {
           this.removeTaggedEntry(altSegments);
-          return this.addAltTagSegments(index, altSegments);
+          return this.addAltTagSegments(prefix, altSegments);
         } else {
           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 => {
         segment.value = TAG_PREFIX + segment.value;
         return segment;
@@ -192,6 +196,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
     if (segment.type === 'tag') {
       let tag = removeTagPrefix(segment.value);
+      this.pause();
       this.addSeriesByTagFunc(tag);
       return;
     }
@@ -236,13 +241,13 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     var oldTarget = this.queryModel.target.target;
     this.updateModelTarget();
 
-    if (this.queryModel.target !== oldTarget) {
+    if (this.queryModel.target !== oldTarget && !this.paused) {
       this.panelCtrl.refresh();
     }
   }
 
   addFunction(funcDef) {
-    var newFunc = gfunc.createFuncInstance(funcDef, {
+    var newFunc = this.datasource.createFuncInstance(funcDef, {
       withDefaultParams: true,
     });
     newFunc.added = true;
@@ -268,11 +273,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   }
 
   addSeriesByTagFunc(tag) {
-    let funcDef = gfunc.getFuncDef('seriesByTag');
-    let newFunc = gfunc.createFuncInstance(funcDef, {
+    let newFunc = this.datasource.createFuncInstance('seriesByTag', {
       withDefaultParams: false,
     });
-    let tagParam = `${tag}=select tag value`;
+    let tagParam = `${tag}=`;
     newFunc.params = [tagParam];
     this.queryModel.addFunction(newFunc);
     newFunc.added = true;
@@ -314,9 +318,9 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     });
   }
 
-  getTagsAsSegments() {
+  getTagsAsSegments(tagPrefix) {
     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 this.uiSegmentSrv.newSegment({
           value: val.text,
@@ -355,7 +359,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
   addNewTag(segment) {
     let newTagKey = segment.value;
-    let newTag = { key: newTagKey, operator: '=', value: 'select tag value' };
+    let newTag = { key: newTagKey, operator: '=', value: '' };
     this.queryModel.addTag(newTag);
     this.targetChanged();
     this.fixTagSegments();
@@ -374,6 +378,15 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   showDelimiter(index) {
     return index !== this.queryModel.tags.length - 1;
   }
+
+  pause() {
+    this.paused = true;
+  }
+
+  unpause() {
+    this.paused = false;
+    this.panelCtrl.refresh();
+  }
 }
 
 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');
     expect(func).toBeTruthy();
     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);
   });
 
@@ -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.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
       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: {
@@ -180,7 +184,21 @@ describe('GraphiteQueryCtrl', function() {
       ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
       ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
       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 = [
         {
           target: 'nested.query.count',
@@ -189,13 +207,9 @@ describe('GraphiteQueryCtrl', function() {
       ];
 
       ctx.ctrl.updateModelTarget();
-    });
 
-    it('target should remain the same', function() {
       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)');
     });
   });
@@ -271,12 +285,12 @@ describe('GraphiteQueryCtrl', 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);
     });
 
     it('should update target', function() {
-      const expected = "seriesByTag('tag1=select tag value')";
+      const expected = "seriesByTag('tag1=')";
       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;
   border: none;
   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 {
   font-size: $font-size-sm;
   margin: $gf-form-margin;
@@ -176,3 +151,34 @@ input[type="text"].tight-form-func-param {
 .query-troubleshooter__body {
   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;
     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', [
+    'jscs',
+    'jshint',
     'sasslint',
     'exec:tslint',
     'no-only-tests'

+ 180 - 3
yarn.lock

@@ -263,6 +263,10 @@ acorn-dynamic-import@^2.0.0:
   dependencies:
     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:
   version "4.1.0"
   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"
   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"
   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"
   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:
   version "0.0.1"
   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"
   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:
   version "0.1.0"
   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"
   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:
   version "2.5.1"
   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"
   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:
   version "3.3.1"
   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"
     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:
   version "0.1.1"
   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"
   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:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
@@ -3266,6 +3304,12 @@ esprima@^4.0.0:
   version "4.0.0"
   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:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
@@ -3300,7 +3344,7 @@ eventemitter3@1.x.x:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508"
 
-eventemitter3@^2.0.2:
+eventemitter3@^2.0.3:
   version "2.0.3"
   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"
   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"
   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"
     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:
   version "2.3.0"
   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"
     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:
   version "0.1.15"
   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"
     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:
   version "0.1.0"
   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"
   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:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
@@ -9713,6 +9868,10 @@ tr46@^1.0.0:
   dependencies:
     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:
   version "1.0.0"
   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"
     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:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -9952,6 +10115,20 @@ unique-string@^1.0.0:
   dependencies:
     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:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"

Some files were not shown because too many files changed in this diff