Quellcode durchsuchen

feat(plugins): migrating graphite query editor to new model

Torkel Ödegaard vor 10 Jahren
Ursprung
Commit
822c8f1575
29 geänderte Dateien mit 1946 neuen und 2035 gelöschten Zeilen
  1. 1 1
      public/app/core/directives/plugin_component.ts
  2. 10 0
      public/app/features/dashboard/dashboardSrv.js
  3. 1 1
      public/app/features/panel/all.js
  4. 2 2
      public/app/features/panel/panel.ts
  5. 10 6
      public/app/features/panel/query_ctrl.ts
  6. 0 3
      public/app/plugins/datasource/graphite/datasource.d.ts
  7. 0 296
      public/app/plugins/datasource/graphite/datasource.js
  8. 281 0
      public/app/plugins/datasource/graphite/datasource.ts
  9. 0 682
      public/app/plugins/datasource/graphite/lexer.js
  10. 678 0
      public/app/plugins/datasource/graphite/lexer.ts
  11. 0 38
      public/app/plugins/datasource/graphite/module.js
  12. 51 0
      public/app/plugins/datasource/graphite/module.ts
  13. 0 265
      public/app/plugins/datasource/graphite/parser.js
  14. 258 0
      public/app/plugins/datasource/graphite/parser.ts
  15. 17 17
      public/app/plugins/datasource/graphite/partials/query.editor.html
  16. 0 292
      public/app/plugins/datasource/graphite/query_ctrl.js
  17. 275 0
      public/app/plugins/datasource/graphite/query_ctrl.ts
  18. 2 2
      public/app/plugins/datasource/graphite/specs/datasource_specs.ts
  19. 118 0
      public/app/plugins/datasource/graphite/specs/lexer_specs.ts
  20. 183 0
      public/app/plugins/datasource/graphite/specs/parser_specs.ts
  21. 50 65
      public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
  22. 2 4
      public/app/plugins/datasource/prometheus/datasource.ts
  23. 1 12
      public/app/plugins/datasource/prometheus/module.ts
  24. 3 3
      public/app/plugins/datasource/prometheus/query_ctrl.ts
  25. 2 2
      public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
  26. 0 33
      public/test/specs/dashboardSrv-specs.js
  27. 0 122
      public/test/specs/lexer-specs.js
  28. 0 188
      public/test/specs/parser-specs.js
  29. 1 1
      tslint.json

+ 1 - 1
public/app/core/directives/plugin_component.ts

@@ -37,7 +37,7 @@ function pluginDirectiveLoader($compile, datasourceSrv) {
               name: 'metrics-query-editor-' + ds.meta.id,
               bindings: {target: "=", panelCtrl: "=", datasource: "="},
               attrs: {"target": "target", "panel-ctrl": "ctrl", datasource: "datasource"},
-              Component: dsModule.MetricsQueryEditor
+              Component: dsModule.QueryCtrl
             };
           });
         });

+ 10 - 0
public/app/features/dashboard/dashboardSrv.js

@@ -194,6 +194,16 @@ function (angular, $, _, moment) {
         moment.utc(date).fromNow();
     };
 
+    p.getNextQueryLetter = function(panel) {
+      var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+      return _.find(letters, function(refId) {
+        return _.every(panel.targets, function(other) {
+          return other.refId !== refId;
+        });
+      });
+    };
+
     p._updateSchema = function(old) {
       var i, j, k;
       var oldVersion = this.schemaVersion;

+ 1 - 1
public/app/features/panel/all.js

@@ -3,6 +3,6 @@ define([
   './panel_directive',
   './solo_panel_ctrl',
   './panel_loader',
-  './query_editor',
+  './query_ctrl',
   './panel_editor_tab',
 ], function () {});

+ 2 - 2
public/app/features/panel/panel.ts

@@ -5,11 +5,11 @@ import config from 'app/core/config';
 import {PanelCtrl} from './panel_ctrl';
 import {MetricsPanelCtrl} from './metrics_panel_ctrl';
 import {PanelDirective} from './panel_directive';
-import {QueryEditorCtrl} from './query_editor';
+import {QueryCtrl} from './query_ctrl';
 
 export {
   PanelCtrl,
   MetricsPanelCtrl,
   PanelDirective,
-  QueryEditorCtrl,
+  QueryCtrl,
 }

+ 10 - 6
public/app/features/panel/query_editor.ts → public/app/features/panel/query_ctrl.ts

@@ -3,7 +3,7 @@
 import angular from 'angular';
 import _ from 'lodash';
 
-export class QueryEditorCtrl {
+export class QueryCtrl {
   target: any;
   datasource: any;
   panelCtrl: any;
@@ -27,22 +27,26 @@ export class QueryEditorCtrl {
     });
   }
 
-  removeDataQuery(query) {
-    this.panel.targets = _.without(this.panel.targets, query);
+  removeQuery() {
+    this.panel.targets = _.without(this.panel.targets, this.target);
     this.panelCtrl.refresh();
   };
 
-  duplicateDataQuery(query) {
-    var clone = angular.copy(query);
+  duplicateQuery() {
+    var clone = angular.copy(this.target);
     clone.refId = this.getNextQueryLetter();
     this.panel.targets.push(clone);
   }
 
-  moveDataQuery(direction) {
+  moveQuery(direction) {
     var index = _.indexOf(this.panel.targets, this.target);
     _.move(this.panel.targets, index, index + direction);
   }
 
+  refresh() {
+    this.panelCtrl.refresh();
+  }
+
   toggleHideQuery() {
     this.target.hide = !this.target.hide;
     this.panelCtrl.refresh();

+ 0 - 3
public/app/plugins/datasource/graphite/datasource.d.ts

@@ -1,3 +0,0 @@
-declare var Datasource: any;
-export default Datasource;
-

+ 0 - 296
public/app/plugins/datasource/graphite/datasource.js

@@ -1,296 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'jquery',
-  'app/core/config',
-  'app/core/utils/datemath',
-  './query_ctrl',
-  './func_editor',
-  './add_graphite_func',
-],
-function (angular, _, $, config, dateMath) {
-  'use strict';
-
-  /** @ngInject */
-  function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
-    this.basicAuth = instanceSettings.basicAuth;
-    this.url = instanceSettings.url;
-    this.name = instanceSettings.name;
-    this.cacheTimeout = instanceSettings.cacheTimeout;
-    this.withCredentials = instanceSettings.withCredentials;
-    this.render_method = instanceSettings.render_method || 'POST';
-
-    this.query = function(options) {
-      try {
-        var graphOptions = {
-          from: this.translateTime(options.rangeRaw.from, false),
-          until: this.translateTime(options.rangeRaw.to, true),
-          targets: options.targets,
-          format: options.format,
-          cacheTimeout: options.cacheTimeout || this.cacheTimeout,
-          maxDataPoints: options.maxDataPoints,
-        };
-
-        var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
-        if (params.length === 0) {
-          return $q.when([]);
-        }
-
-        if (options.format === 'png') {
-          return $q.when(this.url + '/render' + '?' + params.join('&'));
-        }
-
-        var httpOptions = { method: this.render_method, url: '/render' };
-
-        if (httpOptions.method === 'GET') {
-          httpOptions.url = httpOptions.url + '?' + params.join('&');
-        }
-        else {
-          httpOptions.data = params.join('&');
-          httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
-        }
-
-        return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
-      }
-      catch(err) {
-        return $q.reject(err);
-      }
-    };
-
-    this.convertDataPointsToMs = function(result) {
-      if (!result || !result.data) { return []; }
-      for (var i = 0; i < result.data.length; i++) {
-        var series = result.data[i];
-        for (var y = 0; y < series.datapoints.length; y++) {
-          series.datapoints[y][1] *= 1000;
-        }
-      }
-      return result;
-    };
-
-    this.annotationQuery = function(options) {
-      // Graphite metric as annotation
-      if (options.annotation.target) {
-        var target = templateSrv.replace(options.annotation.target);
-        var graphiteQuery = {
-          rangeRaw: options.rangeRaw,
-          targets: [{ target: target }],
-          format: 'json',
-          maxDataPoints: 100
-        };
-
-        return this.query(graphiteQuery)
-        .then(function(result) {
-          var list = [];
-
-          for (var i = 0; i < result.data.length; i++) {
-            var target = result.data[i];
-
-            for (var y = 0; y < target.datapoints.length; y++) {
-              var datapoint = target.datapoints[y];
-              if (!datapoint[0]) { continue; }
-
-              list.push({
-                annotation: options.annotation,
-                time: datapoint[1],
-                title: target.target
-              });
-            }
-          }
-
-          return list;
-        });
-      }
-      // Graphite event as annotation
-      else {
-        var tags = templateSrv.replace(options.annotation.tags);
-        return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
-          var list = [];
-          for (var i = 0; i < results.data.length; i++) {
-            var e = results.data[i];
-
-            list.push({
-              annotation: options.annotation,
-              time: e.when * 1000,
-              title: e.what,
-              tags: e.tags,
-              text: e.data
-            });
-          }
-          return list;
-        });
-      }
-    };
-
-    this.events = function(options) {
-      try {
-        var tags = '';
-        if (options.tags) {
-          tags = '&tags=' + options.tags;
-        }
-
-        return this.doGraphiteRequest({
-          method: 'GET',
-          url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
-            '&until=' + this.translateTime(options.range.to, true) + tags,
-        });
-      }
-      catch(err) {
-        return $q.reject(err);
-      }
-    };
-
-    this.translateTime = function(date, roundUp) {
-      if (_.isString(date)) {
-        if (date === 'now') {
-          return 'now';
-        }
-        else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
-          date = date.substring(3);
-          date = date.replace('m', 'min');
-          date = date.replace('M', 'mon');
-          return date;
-        }
-        date = dateMath.parse(date, roundUp);
-      }
-
-      // graphite' s from filter is exclusive
-      // here we step back one minute in order
-      // to guarantee that we get all the data that
-      // exists for the specified range
-      if (roundUp) {
-        if (date.get('s')) {
-          date.add(1, 'm');
-        }
-      }
-      else if (roundUp === false) {
-        if (date.get('s')) {
-          date.subtract(1, 'm');
-        }
-      }
-
-      return date.unix();
-    };
-
-    this.metricFindQuery = function(query) {
-      var interpolated;
-      try {
-        interpolated = encodeURIComponent(templateSrv.replace(query));
-      }
-      catch(err) {
-        return $q.reject(err);
-      }
-
-      return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
-      .then(function(results) {
-        return _.map(results.data, function(metric) {
-          return {
-            text: metric.text,
-            expandable: metric.expandable ? true : false
-          };
-        });
-      });
-    };
-
-    this.testDatasource = function() {
-      return this.metricFindQuery('*').then(function () {
-        return { status: "success", message: "Data source is working", title: "Success" };
-      });
-    };
-
-    this.listDashboards = function(query) {
-      return this.doGraphiteRequest({ method: 'GET',  url: '/dashboard/find/', params: {query: query || ''} })
-      .then(function(results) {
-        return results.data.dashboards;
-      });
-    };
-
-    this.loadDashboard = function(dashName) {
-      return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
-    };
-
-    this.doGraphiteRequest = function(options) {
-      if (this.basicAuth || this.withCredentials) {
-        options.withCredentials = true;
-      }
-      if (this.basicAuth) {
-        options.headers = options.headers || {};
-        options.headers.Authorization = this.basicAuth;
-      }
-
-      options.url = this.url + options.url;
-      options.inspect = { type: 'graphite' };
-
-      return backendSrv.datasourceRequest(options);
-    };
-
-    this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-
-    this.buildGraphiteParams = function(options, scopedVars) {
-      var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
-      var clean_options = [], targets = {};
-      var target, targetValue, i;
-      var regex = /\#([A-Z])/g;
-      var intervalFormatFixRegex = /'(\d+)m'/gi;
-      var hasTargets = false;
-
-      if (options.format !== 'png') {
-        options['format'] = 'json';
-      }
-
-      function fixIntervalFormat(match) {
-        return match.replace('m', 'min').replace('M', 'mon');
-      }
-
-      for (i = 0; i < options.targets.length; i++) {
-        target = options.targets[i];
-        if (!target.target) {
-          continue;
-        }
-
-        if (!target.refId) {
-          target.refId = this._seriesRefLetters[i];
-        }
-
-        targetValue = templateSrv.replace(target.target, scopedVars);
-        targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
-        targets[target.refId] = targetValue;
-      }
-
-      function nestedSeriesRegexReplacer(match, g1) {
-        return targets[g1];
-      }
-
-      for (i = 0; i < options.targets.length; i++) {
-        target = options.targets[i];
-        if (!target.target) {
-          continue;
-        }
-
-        targetValue = targets[target.refId];
-        targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
-        targets[target.refId] = targetValue;
-
-        if (!target.hide) {
-          hasTargets = true;
-          clean_options.push("target=" + encodeURIComponent(targetValue));
-        }
-      }
-
-      _.each(options, function (value, key) {
-        if ($.inArray(key, graphite_options) === -1) { return; }
-        if (value) {
-          clean_options.push(key + "=" + encodeURIComponent(value));
-        }
-      });
-
-      if (!hasTargets) {
-        return [];
-      }
-
-      return clean_options;
-    };
-  }
-
-  return GraphiteDatasource;
-});

+ 281 - 0
public/app/plugins/datasource/graphite/datasource.ts

@@ -0,0 +1,281 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+import moment from 'moment';
+
+import * as dateMath from 'app/core/utils/datemath';
+
+/** @ngInject */
+export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+  this.basicAuth = instanceSettings.basicAuth;
+  this.url = instanceSettings.url;
+  this.name = instanceSettings.name;
+  this.cacheTimeout = instanceSettings.cacheTimeout;
+  this.withCredentials = instanceSettings.withCredentials;
+  this.render_method = instanceSettings.render_method || 'POST';
+
+  this.query = function(options) {
+    try {
+      var graphOptions = {
+        from: this.translateTime(options.rangeRaw.from, false),
+        until: this.translateTime(options.rangeRaw.to, true),
+        targets: options.targets,
+        format: options.format,
+        cacheTimeout: options.cacheTimeout || this.cacheTimeout,
+        maxDataPoints: options.maxDataPoints,
+      };
+
+      var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
+      if (params.length === 0) {
+        return $q.when([]);
+      }
+
+      if (options.format === 'png') {
+        return $q.when(this.url + '/render' + '?' + params.join('&'));
+      }
+
+      var httpOptions: any = {method: this.render_method, url: '/render'};
+
+      if (httpOptions.method === 'GET') {
+        httpOptions.url = httpOptions.url + '?' + params.join('&');
+      } else {
+        httpOptions.data = params.join('&');
+        httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
+      }
+
+      return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
+    } catch (err) {
+      return $q.reject(err);
+    }
+  };
+
+  this.convertDataPointsToMs = function(result) {
+    if (!result || !result.data) { return []; }
+    for (var i = 0; i < result.data.length; i++) {
+      var series = result.data[i];
+      for (var y = 0; y < series.datapoints.length; y++) {
+        series.datapoints[y][1] *= 1000;
+      }
+    }
+    return result;
+  };
+
+  this.annotationQuery = function(options) {
+    // Graphite metric as annotation
+    if (options.annotation.target) {
+      var target = templateSrv.replace(options.annotation.target);
+      var graphiteQuery = {
+        rangeRaw: options.rangeRaw,
+        targets: [{ target: target }],
+        format: 'json',
+        maxDataPoints: 100
+      };
+
+      return this.query(graphiteQuery)
+      .then(function(result) {
+        var list = [];
+
+        for (var i = 0; i < result.data.length; i++) {
+          var target = result.data[i];
+
+          for (var y = 0; y < target.datapoints.length; y++) {
+            var datapoint = target.datapoints[y];
+            if (!datapoint[0]) { continue; }
+
+            list.push({
+              annotation: options.annotation,
+              time: datapoint[1],
+              title: target.target
+            });
+          }
+        }
+
+        return list;
+      });
+    } else {
+      // Graphite event as annotation
+      var tags = templateSrv.replace(options.annotation.tags);
+      return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
+        var list = [];
+        for (var i = 0; i < results.data.length; i++) {
+          var e = results.data[i];
+
+          list.push({
+            annotation: options.annotation,
+            time: e.when * 1000,
+            title: e.what,
+            tags: e.tags,
+            text: e.data
+          });
+        }
+        return list;
+      });
+    }
+  };
+
+  this.events = function(options) {
+    try {
+      var tags = '';
+      if (options.tags) {
+        tags = '&tags=' + options.tags;
+      }
+
+      return this.doGraphiteRequest({
+        method: 'GET',
+        url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
+          '&until=' + this.translateTime(options.range.to, true) + tags,
+      });
+    } catch (err) {
+      return $q.reject(err);
+    }
+  };
+
+  this.translateTime = function(date, roundUp) {
+    if (_.isString(date)) {
+      if (date === 'now') {
+        return 'now';
+      } else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
+        date = date.substring(3);
+        date = date.replace('m', 'min');
+        date = date.replace('M', 'mon');
+        return date;
+      }
+      date = dateMath.parse(date, roundUp);
+    }
+
+    // graphite' s from filter is exclusive
+    // here we step back one minute in order
+    // to guarantee that we get all the data that
+    // exists for the specified range
+    if (roundUp) {
+      if (date.get('s')) {
+        date.add(1, 'm');
+      }
+    } else if (roundUp === false) {
+      if (date.get('s')) {
+        date.subtract(1, 'm');
+      }
+    }
+
+    return date.unix();
+  };
+
+  this.metricFindQuery = function(query) {
+    var interpolated;
+    try {
+      interpolated = encodeURIComponent(templateSrv.replace(query));
+    } catch (err) {
+      return $q.reject(err);
+    }
+
+    return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
+    .then(function(results) {
+      return _.map(results.data, function(metric) {
+        return {
+          text: metric.text,
+          expandable: metric.expandable ? true : false
+        };
+      });
+    });
+  };
+
+  this.testDatasource = function() {
+    return this.metricFindQuery('*').then(function () {
+      return { status: "success", message: "Data source is working", title: "Success" };
+    });
+  };
+
+  this.listDashboards = function(query) {
+    return this.doGraphiteRequest({ method: 'GET',  url: '/dashboard/find/', params: {query: query || ''} })
+    .then(function(results) {
+      return results.data.dashboards;
+    });
+  };
+
+  this.loadDashboard = function(dashName) {
+    return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
+  };
+
+  this.doGraphiteRequest = function(options) {
+    if (this.basicAuth || this.withCredentials) {
+      options.withCredentials = true;
+    }
+    if (this.basicAuth) {
+      options.headers = options.headers || {};
+      options.headers.Authorization = this.basicAuth;
+    }
+
+    options.url = this.url + options.url;
+    options.inspect = { type: 'graphite' };
+
+    return backendSrv.datasourceRequest(options);
+  };
+
+  this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+  this.buildGraphiteParams = function(options, scopedVars) {
+    var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
+    var clean_options = [], targets = {};
+    var target, targetValue, i;
+    var regex = /\#([A-Z])/g;
+    var intervalFormatFixRegex = /'(\d+)m'/gi;
+    var hasTargets = false;
+
+    if (options.format !== 'png') {
+      options['format'] = 'json';
+    }
+
+    function fixIntervalFormat(match) {
+      return match.replace('m', 'min').replace('M', 'mon');
+    }
+
+    for (i = 0; i < options.targets.length; i++) {
+      target = options.targets[i];
+      if (!target.target) {
+        continue;
+      }
+
+      if (!target.refId) {
+        target.refId = this._seriesRefLetters[i];
+      }
+
+      targetValue = templateSrv.replace(target.target, scopedVars);
+      targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
+      targets[target.refId] = targetValue;
+    }
+
+    function nestedSeriesRegexReplacer(match, g1) {
+      return targets[g1];
+    }
+
+    for (i = 0; i < options.targets.length; i++) {
+      target = options.targets[i];
+      if (!target.target) {
+        continue;
+      }
+
+      targetValue = targets[target.refId];
+      targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
+      targets[target.refId] = targetValue;
+
+      if (!target.hide) {
+        hasTargets = true;
+        clean_options.push("target=" + encodeURIComponent(targetValue));
+      }
+    }
+
+    _.each(options, function (value, key) {
+      if (_.indexOf(graphite_options, key) === -1) { return; }
+      if (value) {
+        clean_options.push(key + "=" + encodeURIComponent(value));
+      }
+    });
+
+    if (!hasTargets) {
+      return [];
+    }
+
+    return clean_options;
+  };
+}

+ 0 - 682
public/app/plugins/datasource/graphite/lexer.js

@@ -1,682 +0,0 @@
-define([
-  'lodash'
-], function(_) {
-  'use strict';
-
-  // This is auto generated from the unicode tables.
-  // The tables are at:
-  // http://www.fileformat.info/info/unicode/category/Lu/list.htm
-  // http://www.fileformat.info/info/unicode/category/Ll/list.htm
-  // http://www.fileformat.info/info/unicode/category/Lt/list.htm
-  // http://www.fileformat.info/info/unicode/category/Lm/list.htm
-  // http://www.fileformat.info/info/unicode/category/Lo/list.htm
-  // http://www.fileformat.info/info/unicode/category/Nl/list.htm
-
-  var unicodeLetterTable = [
-    170, 170, 181, 181, 186, 186, 192, 214,
-    216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
-    880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
-    910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
-    1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
-    1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
-    1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
-    1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
-    2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
-    2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
-    2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
-    2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
-    2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
-    2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
-    2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
-    2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
-    2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
-    2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
-    2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
-    2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
-    3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
-    3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
-    3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
-    3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
-    3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
-    3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
-    3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
-    3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
-    3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
-    3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
-    3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
-    4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
-    4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
-    4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
-    4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
-    4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
-    4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
-    5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
-    5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
-    6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
-    6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
-    6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
-    6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
-    7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
-    7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
-    8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
-    8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
-    8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
-    8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
-    8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
-    8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
-    8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
-    11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
-    11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
-    11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
-    11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
-    12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
-    12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
-    12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
-    19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
-    42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
-    42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
-    42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
-    43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
-    43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
-    43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
-    43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
-    43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
-    43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
-    43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
-    43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
-    55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
-    64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
-    64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
-    64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
-    64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
-    65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
-    65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
-    65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
-    65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
-    65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
-    66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
-    66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
-    67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
-    67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
-    68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
-    68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
-    69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
-    92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
-    119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
-    119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
-    120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
-    120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
-    120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
-    120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
-    120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
-    131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
-    177984, 177984, 178205, 178205, 194560, 195101
-  ];
-
-  var identifierStartTable = [];
-
-  for (var i = 0; i < 128; i++) {
-    identifierStartTable[i] =
-      i >= 48 && i <= 57 || // 0-9
-      i === 36 ||           // $
-      i === 126 ||          // ~
-      i === 124 ||          // |
-      i >= 65 && i <= 90 || // A-Z
-      i === 95 ||           // _
-      i === 45 ||           // -
-      i === 42 ||           // *
-      i === 58 ||           // :
-      i === 91 ||           // templateStart [
-      i === 93 ||           // templateEnd ]
-      i === 63 ||           // ?
-      i === 37 ||           // %
-      i === 35 ||           // #
-      i === 61 ||           // =
-      i >= 97 && i <= 122;  // a-z
-  }
-
-  var identifierPartTable = [];
-
-  for (var i2 = 0; i2 < 128; i2++) {
-    identifierPartTable[i2] =
-      identifierStartTable[i2] || // $, _, A-Z, a-z
-      i2 >= 48 && i2 <= 57;        // 0-9
-  }
-
-  function Lexer(expression) {
-    this.input = expression;
-    this.char = 1;
-    this.from = 1;
-  }
-
-  Lexer.prototype = {
-
-    peek: function (i) {
-      return this.input.charAt(i || 0);
-    },
-
-    skip: function (i) {
-      i = i || 1;
-      this.char += i;
-      this.input = this.input.slice(i);
-    },
-
-    tokenize: function() {
-      var list = [];
-      var token;
-      while (token = this.next()) {
-        list.push(token);
-      }
-      return list;
-    },
-
-    next: function() {
-      this.from = this.char;
-
-      // Move to the next non-space character.
-      var start;
-      if (/\s/.test(this.peek())) {
-        start = this.char;
-
-        while (/\s/.test(this.peek())) {
-          this.from += 1;
-          this.skip();
-        }
-
-        if (this.peek() === "") { // EOL
-          return null;
-        }
-      }
-
-      var match = this.scanStringLiteral();
-      if (match) {
-        return match;
-      }
-
-      match =
-        this.scanPunctuator() ||
-        this.scanNumericLiteral() ||
-        this.scanIdentifier() ||
-        this.scanTemplateSequence();
-
-      if (match) {
-        this.skip(match.value.length);
-        return match;
-      }
-
-      // No token could be matched, give up.
-      return null;
-    },
-
-    scanTemplateSequence: function() {
-      if (this.peek() === '[' && this.peek(1) === '[') {
-        return {
-          type: 'templateStart',
-          value: '[[',
-          pos: this.char
-        };
-      }
-
-      if (this.peek() === ']' && this.peek(1) === ']') {
-        return {
-          type: 'templateEnd',
-          value: '[[',
-          pos: this.char
-        };
-      }
-
-      return null;
-    },
-
-      /*
-     * Extract a JavaScript identifier out of the next sequence of
-     * characters or return 'null' if its not possible. In addition,
-     * to Identifier this method can also produce BooleanLiteral
-     * (true/false) and NullLiteral (null).
-     */
-    scanIdentifier: function() {
-      var id = "";
-      var index = 0;
-      var type, char;
-
-      // Detects any character in the Unicode categories "Uppercase
-      // letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
-      // (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
-      // "Letter number (Nl)".
-      //
-      // Both approach and unicodeLetterTable were borrowed from
-      // Google's Traceur.
-
-      function isUnicodeLetter(code) {
-        for (var i = 0; i < unicodeLetterTable.length;) {
-          if (code < unicodeLetterTable[i++]) {
-            return false;
-          }
-
-          if (code <= unicodeLetterTable[i++]) {
-            return true;
-          }
-        }
-
-        return false;
-      }
-
-      function isHexDigit(str) {
-        return (/^[0-9a-fA-F]$/).test(str);
-      }
-
-      var readUnicodeEscapeSequence = _.bind(function () {
-        /*jshint validthis:true */
-        index += 1;
-
-        if (this.peek(index) !== "u") {
-          return null;
-        }
-
-        var ch1 = this.peek(index + 1);
-        var ch2 = this.peek(index + 2);
-        var ch3 = this.peek(index + 3);
-        var ch4 = this.peek(index + 4);
-        var code;
-
-        if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
-          code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
-
-          if (isUnicodeLetter(code)) {
-            index += 5;
-            return "\\u" + ch1 + ch2 + ch3 + ch4;
-          }
-
-          return null;
-        }
-
-        return null;
-      }, this);
-
-      var getIdentifierStart = _.bind(function () {
-        /*jshint validthis:true */
-        var chr = this.peek(index);
-        var code = chr.charCodeAt(0);
-
-        if (chr === '*') {
-          index += 1;
-          return chr;
-        }
-
-        if (code === 92) {
-          return readUnicodeEscapeSequence();
-        }
-
-        if (code < 128) {
-          if (identifierStartTable[code]) {
-            index += 1;
-            return chr;
-          }
-
-          return null;
-        }
-
-        if (isUnicodeLetter(code)) {
-          index += 1;
-          return chr;
-        }
-
-        return null;
-      }, this);
-
-      var getIdentifierPart = _.bind(function () {
-        /*jshint validthis:true */
-        var chr = this.peek(index);
-        var code = chr.charCodeAt(0);
-
-        if (code === 92) {
-          return readUnicodeEscapeSequence();
-        }
-
-        if (code < 128) {
-          if (identifierPartTable[code]) {
-            index += 1;
-            return chr;
-          }
-
-          return null;
-        }
-
-        if (isUnicodeLetter(code)) {
-          index += 1;
-          return chr;
-        }
-
-        return null;
-      }, this);
-
-      char = getIdentifierStart();
-      if (char === null) {
-        return null;
-      }
-
-      id = char;
-      for (;;) {
-        char = getIdentifierPart();
-
-        if (char === null) {
-          break;
-        }
-
-        id += char;
-      }
-
-      switch (id) {
-      case 'true': {
-        type = 'bool';
-        break;
-      }
-      case 'false': {
-        type = 'bool';
-        break;
-      }
-      default:
-        type = "identifier";
-      }
-
-      return {
-        type: type,
-        value: id,
-        pos: this.char
-      };
-
-    },
-
-    /*
-     * Extract a numeric literal out of the next sequence of
-     * characters or return 'null' if its not possible. This method
-     * supports all numeric literals described in section 7.8.3
-     * of the EcmaScript 5 specification.
-     *
-     * This method's implementation was heavily influenced by the
-     * scanNumericLiteral function in the Esprima parser's source code.
-     */
-    scanNumericLiteral: function () {
-      var index = 0;
-      var value = "";
-      var length = this.input.length;
-      var char = this.peek(index);
-      var bad;
-
-      function isDecimalDigit(str) {
-        return (/^[0-9]$/).test(str);
-      }
-
-      function isOctalDigit(str) {
-        return (/^[0-7]$/).test(str);
-      }
-
-      function isHexDigit(str) {
-        return (/^[0-9a-fA-F]$/).test(str);
-      }
-
-      function isIdentifierStart(ch) {
-        return (ch === "$") || (ch === "_") || (ch === "\\") ||
-          (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
-      }
-
-      // handle negative num literals
-      if (char === '-') {
-        value += char;
-        index += 1;
-        char = this.peek(index);
-      }
-
-      // Numbers must start either with a decimal digit or a point.
-      if (char !== "." && !isDecimalDigit(char)) {
-        return null;
-      }
-
-      if (char !== ".") {
-        value += this.peek(index);
-        index += 1;
-        char = this.peek(index);
-
-        if (value === "0") {
-          // Base-16 numbers.
-          if (char === "x" || char === "X") {
-            index += 1;
-            value += char;
-
-            while (index < length) {
-              char = this.peek(index);
-              if (!isHexDigit(char)) {
-                break;
-              }
-              value += char;
-              index += 1;
-            }
-
-            if (value.length <= 2) { // 0x
-              return {
-                type: 'number',
-                value: value,
-                isMalformed: true,
-                pos: this.char
-              };
-            }
-
-            if (index < length) {
-              char = this.peek(index);
-              if (isIdentifierStart(char)) {
-                return null;
-              }
-            }
-
-            return {
-              type: 'number',
-              value: value,
-              base: 16,
-              isMalformed: false,
-              pos: this.char
-            };
-          }
-
-          // Base-8 numbers.
-          if (isOctalDigit(char)) {
-            index += 1;
-            value += char;
-            bad = false;
-
-            while (index < length) {
-              char = this.peek(index);
-
-              // Numbers like '019' (note the 9) are not valid octals
-              // but we still parse them and mark as malformed.
-
-              if (isDecimalDigit(char)) {
-                bad = true;
-              } else if (!isOctalDigit(char)) {
-                break;
-              }
-              value += char;
-              index += 1;
-            }
-
-            if (index < length) {
-              char = this.peek(index);
-              if (isIdentifierStart(char)) {
-                return null;
-              }
-            }
-
-            return {
-              type: 'number',
-              value: value,
-              base: 8,
-              isMalformed: false
-            };
-          }
-
-          // Decimal numbers that start with '0' such as '09' are illegal
-          // but we still parse them and return as malformed.
-
-          if (isDecimalDigit(char)) {
-            index += 1;
-            value += char;
-          }
-        }
-
-        while (index < length) {
-          char = this.peek(index);
-          if (!isDecimalDigit(char)) {
-            break;
-          }
-          value += char;
-          index += 1;
-        }
-      }
-
-      // Decimal digits.
-
-      if (char === ".") {
-        value += char;
-        index += 1;
-
-        while (index < length) {
-          char = this.peek(index);
-          if (!isDecimalDigit(char)) {
-            break;
-          }
-          value += char;
-          index += 1;
-        }
-      }
-
-      // Exponent part.
-
-      if (char === "e" || char === "E") {
-        value += char;
-        index += 1;
-        char = this.peek(index);
-
-        if (char === "+" || char === "-") {
-          value += this.peek(index);
-          index += 1;
-        }
-
-        char = this.peek(index);
-        if (isDecimalDigit(char)) {
-          value += char;
-          index += 1;
-
-          while (index < length) {
-            char = this.peek(index);
-            if (!isDecimalDigit(char)) {
-              break;
-            }
-            value += char;
-            index += 1;
-          }
-        } else {
-          return null;
-        }
-      }
-
-      if (index < length) {
-        char = this.peek(index);
-        if (!this.isPunctuator(char)) {
-          return null;
-        }
-      }
-
-      return {
-        type: 'number',
-        value: value,
-        base: 10,
-        pos: this.char,
-        isMalformed: !isFinite(value)
-      };
-    },
-
-    isPunctuator: function (ch1) {
-      switch (ch1) {
-      case ".":
-      case "(":
-      case ")":
-      case ",":
-      case "{":
-      case "}":
-        return true;
-      }
-
-      return false;
-    },
-
-    scanPunctuator: function () {
-      var ch1 = this.peek();
-
-      if (this.isPunctuator(ch1)) {
-        return {
-          type: ch1,
-          value: ch1,
-          pos: this.char
-        };
-      }
-
-      return null;
-    },
-
-      /*
-     * Extract a string out of the next sequence of characters and/or
-     * lines or return 'null' if its not possible. Since strings can
-     * span across multiple lines this method has to move the char
-     * pointer.
-     *
-     * This method recognizes pseudo-multiline JavaScript strings:
-     *
-     *   var str = "hello\
-     *   world";
-     */
-    scanStringLiteral: function () {
-      /*jshint loopfunc:true */
-      var quote = this.peek();
-
-      // String must start with a quote.
-      if (quote !== "\"" && quote !== "'") {
-        return null;
-      }
-
-      var value = "";
-
-      this.skip();
-
-      while (this.peek() !== quote) {
-        if (this.peek() === "") { // End Of Line
-          return {
-            type: 'string',
-            value: value,
-            isUnclosed: true,
-            quote: quote,
-            pos: this.char
-          };
-        }
-
-        var char = this.peek();
-        var jump = 1; // A length of a jump, after we're done
-                      // parsing this character.
-
-        value += char;
-        this.skip(jump);
-      }
-
-      this.skip();
-      return {
-        type: 'string',
-        value: value,
-        isUnclosed: false,
-        quote: quote,
-        pos: this.char
-      };
-    },
-
-  };
-
-  return Lexer;
-
-});

+ 678 - 0
public/app/plugins/datasource/graphite/lexer.ts

@@ -0,0 +1,678 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import _ from 'lodash';
+
+// This is auto generated from the unicode tables.
+// The tables are at:
+// http://www.fileformat.info/info/unicode/category/Lu/list.htm
+// http://www.fileformat.info/info/unicode/category/Ll/list.htm
+// http://www.fileformat.info/info/unicode/category/Lt/list.htm
+// http://www.fileformat.info/info/unicode/category/Lm/list.htm
+// http://www.fileformat.info/info/unicode/category/Lo/list.htm
+// http://www.fileformat.info/info/unicode/category/Nl/list.htm
+
+var unicodeLetterTable = [
+  170, 170, 181, 181, 186, 186, 192, 214,
+  216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
+  880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
+  910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
+  1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
+  1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
+  1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
+  1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
+  2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
+  2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
+  2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
+  2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
+  2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
+  2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
+  2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
+  2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
+  2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
+  2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
+  2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
+  2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
+  3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
+  3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
+  3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
+  3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
+  3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
+  3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
+  3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
+  3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
+  3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
+  3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
+  3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
+  4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
+  4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
+  4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
+  4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
+  4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
+  4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
+  5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
+  5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
+  6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
+  6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
+  6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
+  6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
+  7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
+  7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
+  8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
+  8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
+  8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
+  8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
+  8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
+  8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
+  8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
+  11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
+  11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
+  11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
+  11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
+  12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
+  12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
+  12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
+  19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
+  42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
+  42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
+  42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
+  43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
+  43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
+  43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
+  43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
+  43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
+  43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
+  43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
+  43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
+  55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
+  64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
+  64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
+  64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
+  64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
+  65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
+  65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
+  65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
+  65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
+  65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
+  66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
+  66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
+  67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
+  67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
+  68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
+  68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
+  69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
+  92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
+  119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
+  119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
+  120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
+  120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
+  120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
+  120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
+  120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
+  131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
+  177984, 177984, 178205, 178205, 194560, 195101
+];
+
+var identifierStartTable = [];
+
+for (var i = 0; i < 128; i++) {
+  identifierStartTable[i] =
+    i >= 48 && i <= 57 || // 0-9
+    i === 36 ||           // $
+    i === 126 ||          // ~
+    i === 124 ||          // |
+    i >= 65 && i <= 90 || // A-Z
+    i === 95 ||           // _
+    i === 45 ||           // -
+    i === 42 ||           // *
+    i === 58 ||           // :
+    i === 91 ||           // templateStart [
+    i === 93 ||           // templateEnd ]
+    i === 63 ||           // ?
+    i === 37 ||           // %
+    i === 35 ||           // #
+    i === 61 ||           // =
+    i >= 97 && i <= 122;  // a-z
+}
+
+var identifierPartTable = [];
+
+for (var i2 = 0; i2 < 128; i2++) {
+  identifierPartTable[i2] =
+    identifierStartTable[i2] || // $, _, A-Z, a-z
+    i2 >= 48 && i2 <= 57;        // 0-9
+}
+
+export function Lexer(expression) {
+  this.input = expression;
+  this.char = 1;
+  this.from = 1;
+}
+
+Lexer.prototype = {
+
+  peek: function (i) {
+    return this.input.charAt(i || 0);
+  },
+
+  skip: function (i) {
+    i = i || 1;
+    this.char += i;
+    this.input = this.input.slice(i);
+  },
+
+  tokenize: function() {
+    var list = [];
+    var token;
+    while (token = this.next()) {
+      list.push(token);
+    }
+    return list;
+  },
+
+  next: function() {
+    this.from = this.char;
+
+    // Move to the next non-space character.
+    var start;
+    if (/\s/.test(this.peek())) {
+      start = this.char;
+
+      while (/\s/.test(this.peek())) {
+        this.from += 1;
+        this.skip();
+      }
+
+      if (this.peek() === "") { // EOL
+        return null;
+      }
+    }
+
+    var match = this.scanStringLiteral();
+    if (match) {
+      return match;
+    }
+
+    match =
+      this.scanPunctuator() ||
+      this.scanNumericLiteral() ||
+      this.scanIdentifier() ||
+      this.scanTemplateSequence();
+
+    if (match) {
+      this.skip(match.value.length);
+      return match;
+    }
+
+    // No token could be matched, give up.
+    return null;
+  },
+
+  scanTemplateSequence: function() {
+    if (this.peek() === '[' && this.peek(1) === '[') {
+      return {
+        type: 'templateStart',
+        value: '[[',
+        pos: this.char
+      };
+    }
+
+    if (this.peek() === ']' && this.peek(1) === ']') {
+      return {
+        type: 'templateEnd',
+        value: '[[',
+        pos: this.char
+      };
+    }
+
+    return null;
+  },
+
+  /*
+   * Extract a JavaScript identifier out of the next sequence of
+   * characters or return 'null' if its not possible. In addition,
+   * to Identifier this method can also produce BooleanLiteral
+   * (true/false) and NullLiteral (null).
+   */
+  scanIdentifier: function() {
+    var id = "";
+    var index = 0;
+    var type, char;
+
+    // Detects any character in the Unicode categories "Uppercase
+    // letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
+    // (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
+    // "Letter number (Nl)".
+    //
+    // Both approach and unicodeLetterTable were borrowed from
+    // Google's Traceur.
+
+    function isUnicodeLetter(code) {
+      for (var i = 0; i < unicodeLetterTable.length;) {
+        if (code < unicodeLetterTable[i++]) {
+          return false;
+        }
+
+        if (code <= unicodeLetterTable[i++]) {
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+    function isHexDigit(str) {
+      return (/^[0-9a-fA-F]$/).test(str);
+    }
+
+    var readUnicodeEscapeSequence = _.bind(function () {
+      /*jshint validthis:true */
+      index += 1;
+
+      if (this.peek(index) !== "u") {
+        return null;
+      }
+
+      var ch1 = this.peek(index + 1);
+      var ch2 = this.peek(index + 2);
+      var ch3 = this.peek(index + 3);
+      var ch4 = this.peek(index + 4);
+      var code;
+
+      if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
+        code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
+
+        if (isUnicodeLetter(code)) {
+          index += 5;
+          return "\\u" + ch1 + ch2 + ch3 + ch4;
+        }
+
+        return null;
+      }
+
+      return null;
+    }, this);
+
+    var getIdentifierStart = _.bind(function () {
+      /*jshint validthis:true */
+      var chr = this.peek(index);
+      var code = chr.charCodeAt(0);
+
+      if (chr === '*') {
+        index += 1;
+        return chr;
+      }
+
+      if (code === 92) {
+        return readUnicodeEscapeSequence();
+      }
+
+      if (code < 128) {
+        if (identifierStartTable[code]) {
+          index += 1;
+          return chr;
+        }
+
+        return null;
+      }
+
+      if (isUnicodeLetter(code)) {
+        index += 1;
+        return chr;
+      }
+
+      return null;
+    }, this);
+
+    var getIdentifierPart = _.bind(function () {
+      /*jshint validthis:true */
+      var chr = this.peek(index);
+      var code = chr.charCodeAt(0);
+
+      if (code === 92) {
+        return readUnicodeEscapeSequence();
+      }
+
+      if (code < 128) {
+        if (identifierPartTable[code]) {
+          index += 1;
+          return chr;
+        }
+
+        return null;
+      }
+
+      if (isUnicodeLetter(code)) {
+        index += 1;
+        return chr;
+      }
+
+      return null;
+    }, this);
+
+    char = getIdentifierStart();
+    if (char === null) {
+      return null;
+    }
+
+    id = char;
+    for (;;) {
+      char = getIdentifierPart();
+
+      if (char === null) {
+        break;
+      }
+
+      id += char;
+    }
+
+    switch (id) {
+      case 'true': {
+        type = 'bool';
+        break;
+      }
+      case 'false': {
+        type = 'bool';
+        break;
+      }
+      default:
+        type = "identifier";
+    }
+
+    return {
+      type: type,
+      value: id,
+      pos: this.char
+    };
+
+  },
+
+  /*
+   * Extract a numeric literal out of the next sequence of
+   * characters or return 'null' if its not possible. This method
+   * supports all numeric literals described in section 7.8.3
+   * of the EcmaScript 5 specification.
+   *
+   * This method's implementation was heavily influenced by the
+   * scanNumericLiteral function in the Esprima parser's source code.
+   */
+  scanNumericLiteral: function (): any {
+    var index = 0;
+    var value = "";
+    var length = this.input.length;
+    var char = this.peek(index);
+    var bad;
+
+    function isDecimalDigit(str) {
+      return (/^[0-9]$/).test(str);
+    }
+
+    function isOctalDigit(str) {
+      return (/^[0-7]$/).test(str);
+    }
+
+    function isHexDigit(str) {
+      return (/^[0-9a-fA-F]$/).test(str);
+    }
+
+    function isIdentifierStart(ch) {
+      return (ch === "$") || (ch === "_") || (ch === "\\") ||
+        (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
+    }
+
+    // handle negative num literals
+    if (char === '-') {
+      value += char;
+      index += 1;
+    char = this.peek(index);
+      }
+
+      // Numbers must start either with a decimal digit or a point.
+      if (char !== "." && !isDecimalDigit(char)) {
+        return null;
+      }
+
+      if (char !== ".") {
+        value += this.peek(index);
+        index += 1;
+        char = this.peek(index);
+
+        if (value === "0") {
+          // Base-16 numbers.
+          if (char === "x" || char === "X") {
+            index += 1;
+            value += char;
+
+            while (index < length) {
+              char = this.peek(index);
+              if (!isHexDigit(char)) {
+                break;
+              }
+              value += char;
+              index += 1;
+            }
+
+            if (value.length <= 2) { // 0x
+              return {
+                type: 'number',
+                value: value,
+                isMalformed: true,
+                pos: this.char
+              };
+            }
+
+            if (index < length) {
+              char = this.peek(index);
+              if (isIdentifierStart(char)) {
+                return null;
+              }
+            }
+
+            return {
+              type: 'number',
+              value: value,
+              base: 16,
+              isMalformed: false,
+              pos: this.char
+            };
+          }
+
+          // Base-8 numbers.
+          if (isOctalDigit(char)) {
+            index += 1;
+            value += char;
+            bad = false;
+
+            while (index < length) {
+              char = this.peek(index);
+
+              // Numbers like '019' (note the 9) are not valid octals
+              // but we still parse them and mark as malformed.
+
+              if (isDecimalDigit(char)) {
+                bad = true;
+              } else if (!isOctalDigit(char)) {
+                break;
+              }
+              value += char;
+              index += 1;
+            }
+
+            if (index < length) {
+              char = this.peek(index);
+              if (isIdentifierStart(char)) {
+                return null;
+              }
+            }
+
+            return {
+              type: 'number',
+              value: value,
+              base: 8,
+              isMalformed: false
+            };
+          }
+
+          // Decimal numbers that start with '0' such as '09' are illegal
+          // but we still parse them and return as malformed.
+
+          if (isDecimalDigit(char)) {
+            index += 1;
+            value += char;
+          }
+        }
+
+        while (index < length) {
+          char = this.peek(index);
+          if (!isDecimalDigit(char)) {
+            break;
+          }
+          value += char;
+          index += 1;
+        }
+      }
+
+      // Decimal digits.
+
+      if (char === ".") {
+        value += char;
+        index += 1;
+
+        while (index < length) {
+          char = this.peek(index);
+          if (!isDecimalDigit(char)) {
+            break;
+          }
+          value += char;
+          index += 1;
+        }
+      }
+
+      // Exponent part.
+
+      if (char === "e" || char === "E") {
+        value += char;
+        index += 1;
+        char = this.peek(index);
+
+        if (char === "+" || char === "-") {
+          value += this.peek(index);
+          index += 1;
+        }
+
+        char = this.peek(index);
+        if (isDecimalDigit(char)) {
+          value += char;
+          index += 1;
+
+          while (index < length) {
+            char = this.peek(index);
+            if (!isDecimalDigit(char)) {
+              break;
+            }
+            value += char;
+            index += 1;
+          }
+        } else {
+          return null;
+        }
+      }
+
+      if (index < length) {
+        char = this.peek(index);
+        if (!this.isPunctuator(char)) {
+          return null;
+        }
+      }
+
+      return {
+        type: 'number',
+        value: value,
+        base: 10,
+        pos: this.char,
+        isMalformed: !isFinite(+value)
+      };
+    },
+
+    isPunctuator: function (ch1) {
+      switch (ch1) {
+        case ".":
+          case "(":
+          case ")":
+          case ",":
+          case "{":
+          case "}":
+          return true;
+      }
+
+      return false;
+    },
+
+    scanPunctuator: function () {
+      var ch1 = this.peek();
+
+      if (this.isPunctuator(ch1)) {
+        return {
+          type: ch1,
+          value: ch1,
+          pos: this.char
+        };
+      }
+
+      return null;
+    },
+
+    /*
+     * Extract a string out of the next sequence of characters and/or
+     * lines or return 'null' if its not possible. Since strings can
+     * span across multiple lines this method has to move the char
+     * pointer.
+     *
+     * This method recognizes pseudo-multiline JavaScript strings:
+     *
+     *   var str = "hello\
+     *   world";
+     */
+    scanStringLiteral: function () {
+      /*jshint loopfunc:true */
+      var quote = this.peek();
+
+      // String must start with a quote.
+      if (quote !== "\"" && quote !== "'") {
+        return null;
+      }
+
+      var value = "";
+
+      this.skip();
+
+      while (this.peek() !== quote) {
+        if (this.peek() === "") { // End Of Line
+          return {
+            type: 'string',
+            value: value,
+            isUnclosed: true,
+            quote: quote,
+            pos: this.char
+          };
+        }
+
+        var char = this.peek();
+        var jump = 1; // A length of a jump, after we're done
+        // parsing this character.
+
+        value += char;
+        this.skip(jump);
+      }
+
+      this.skip();
+      return {
+        type: 'string',
+        value: value,
+        isUnclosed: false,
+        quote: quote,
+        pos: this.char
+      };
+    },
+
+  };
+

+ 0 - 38
public/app/plugins/datasource/graphite/module.js

@@ -1,38 +0,0 @@
-define([
-  './datasource',
-],
-function (GraphiteDatasource) {
-  'use strict';
-
-  function metricsQueryEditor() {
-    return {
-      controller: 'GraphiteQueryCtrl',
-      templateUrl: 'public/app/plugins/datasource/graphite/partials/query.editor.html'
-    };
-  }
-
-  function metricsQueryOptions() {
-    return {templateUrl: 'public/app/plugins/datasource/graphite/partials/query.options.html'};
-  }
-
-  function annotationsQueryEditor() {
-    return {templateUrl: 'public/app/plugins/datasource/graphite/partials/annotations.editor.html'};
-  }
-
-  function configView() {
-    return {templateUrl: 'public/app/plugins/datasource/graphite/partials/config.html'};
-  }
-
-  function ConfigView() {
-  }
-  ConfigView.templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
-
-  return {
-    Datasource: GraphiteDatasource,
-    configView: configView,
-    annotationsQueryEditor: annotationsQueryEditor,
-    metricsQueryEditor: metricsQueryEditor,
-    metricsQueryOptions: metricsQueryOptions,
-    ConfigView: ConfigView
-  };
-});

+ 51 - 0
public/app/plugins/datasource/graphite/module.ts

@@ -0,0 +1,51 @@
+import {GraphiteDatasource} from './datasource';
+import {GraphiteQueryCtrl} from './query_ctrl';
+
+class GraphiteConfigView {
+  static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
+}
+
+export {
+  GraphiteDatasource as Datasource,
+  GraphiteQueryCtrl as QueryCtrl,
+  GraphiteConfigView as ConfigView
+};
+
+// define([
+//   './datasource',
+// ],
+// function (GraphiteDatasource) {
+//   'use strict';
+//
+//   function metricsQueryEditor() {
+//     return {
+//       controller: 'GraphiteQueryCtrl',
+//       templateUrl: 'public/app/plugins/datasource/graphite/partials/query.editor.html'
+//     };
+//   }
+//
+//   function metricsQueryOptions() {
+//     return {templateUrl: 'public/app/plugins/datasource/graphite/partials/query.options.html'};
+//   }
+//
+//   function annotationsQueryEditor() {
+//     return {templateUrl: 'public/app/plugins/datasource/graphite/partials/annotations.editor.html'};
+//   }
+//
+//   function configView() {
+//     return {templateUrl: 'public/app/plugins/datasource/graphite/partials/config.html'};
+//   }
+//
+//   function ConfigView() {
+//   }
+//   ConfigView.templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
+//
+//   return {
+//     Datasource: GraphiteDatasource,
+//     configView: configView,
+//     annotationsQueryEditor: annotationsQueryEditor,
+//     metricsQueryEditor: metricsQueryEditor,
+//     metricsQueryOptions: metricsQueryOptions,
+//     ConfigView: ConfigView
+//   };
+// });

+ 0 - 265
public/app/plugins/datasource/graphite/parser.js

@@ -1,265 +0,0 @@
-define([
-  './lexer'
-], function (Lexer) {
-  'use strict';
-
-  function Parser(expression) {
-    this.expression = expression;
-    this.lexer = new Lexer(expression);
-    this.tokens = this.lexer.tokenize();
-    this.index = 0;
-  }
-
-  Parser.prototype = {
-
-    getAst: function () {
-      return this.start();
-    },
-
-    start: function () {
-      try {
-        return this.functionCall() || this.metricExpression();
-      }
-      catch (e) {
-        return {
-          type: 'error',
-          message: e.message,
-          pos: e.pos
-        };
-      }
-    },
-
-    curlyBraceSegment: function() {
-      if (this.match('identifier', '{') || this.match('{')) {
-
-        var curlySegment = "";
-
-        while (!this.match('') && !this.match('}')) {
-          curlySegment += this.consumeToken().value;
-        }
-
-        if (!this.match('}')) {
-          this.errorMark("Expected closing '}'");
-        }
-
-        curlySegment += this.consumeToken().value;
-
-        // if curly segment is directly followed by identifier
-        // include it in the segment
-        if (this.match('identifier')) {
-          curlySegment += this.consumeToken().value;
-        }
-
-        return {
-          type: 'segment',
-          value: curlySegment
-        };
-      }
-      else {
-        return null;
-      }
-    },
-
-    metricSegment: function() {
-      var curly = this.curlyBraceSegment();
-      if (curly) {
-        return curly;
-      }
-
-      if (this.match('identifier') || this.match('number')) {
-        // hack to handle float numbers in metric segments
-        var parts = this.consumeToken().value.split('.');
-        if (parts.length === 2) {
-          this.tokens.splice(this.index, 0, { type: '.' });
-          this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
-        }
-
-        return {
-          type: 'segment',
-          value: parts[0]
-        };
-      }
-
-      if (!this.match('templateStart')) {
-        this.errorMark('Expected metric identifier');
-      }
-
-      this.consumeToken();
-
-      if (!this.match('identifier')) {
-        this.errorMark('Expected identifier after templateStart');
-      }
-
-      var node = {
-        type: 'template',
-        value: this.consumeToken().value
-      };
-
-      if (!this.match('templateEnd')) {
-        this.errorMark('Expected templateEnd');
-      }
-
-      this.consumeToken();
-      return node;
-    },
-
-    metricExpression: function() {
-      if (!this.match('templateStart') &&
-          !this.match('identifier') &&
-          !this.match('number') &&
-          !this.match('{')) {
-        return null;
-      }
-
-      var node = {
-        type: 'metric',
-        segments: []
-      };
-
-      node.segments.push(this.metricSegment());
-
-      while (this.match('.')) {
-        this.consumeToken();
-
-        var segment = this.metricSegment();
-        if (!segment) {
-          this.errorMark('Expected metric identifier');
-        }
-
-        node.segments.push(segment);
-      }
-
-      return node;
-    },
-
-    functionCall: function() {
-      if (!this.match('identifier', '(')) {
-        return null;
-      }
-
-      var node = {
-        type: 'function',
-        name: this.consumeToken().value,
-      };
-
-      // consume left parenthesis
-      this.consumeToken();
-
-      node.params = this.functionParameters();
-
-      if (!this.match(')')) {
-        this.errorMark('Expected closing parenthesis');
-      }
-
-      this.consumeToken();
-
-      return node;
-    },
-
-    boolExpression: function() {
-      if (!this.match('bool')) {
-        return null;
-      }
-
-      return {
-        type: 'bool',
-        value: this.consumeToken().value === 'true',
-      };
-    },
-
-    functionParameters: function () {
-      if (this.match(')') || this.match('')) {
-        return [];
-      }
-
-      var param =
-        this.functionCall() ||
-        this.numericLiteral() ||
-        this.seriesRefExpression() ||
-        this.boolExpression() ||
-        this.metricExpression() ||
-        this.stringLiteral();
-
-      if (!this.match(',')) {
-        return [param];
-      }
-
-      this.consumeToken();
-      return [param].concat(this.functionParameters());
-    },
-
-    seriesRefExpression: function() {
-      if (!this.match('identifier')) {
-        return null;
-      }
-
-      var value = this.tokens[this.index].value;
-      if (!value.match(/\#[A-Z]/)) {
-        return null;
-      }
-
-      var token = this.consumeToken();
-
-      return {
-        type: 'series-ref',
-        value: token.value
-      };
-    },
-
-    numericLiteral: function () {
-      if (!this.match('number')) {
-        return null;
-      }
-
-      return {
-        type: 'number',
-        value: parseFloat(this.consumeToken().value)
-      };
-    },
-
-    stringLiteral: function () {
-      if (!this.match('string')) {
-        return null;
-      }
-
-      var token = this.consumeToken();
-      if (token.isUnclosed) {
-        throw { message: 'Unclosed string parameter', pos: token.pos };
-      }
-
-      return {
-        type: 'string',
-        value: token.value
-      };
-    },
-
-    errorMark: function(text) {
-      var currentToken = this.tokens[this.index];
-      var type = currentToken ? currentToken.type : 'end of string';
-      throw {
-        message: text + " instead found " + type,
-        pos: currentToken ? currentToken.pos : this.lexer.char
-      };
-    },
-
-    // returns token value and incre
-    consumeToken: function() {
-      this.index++;
-      return this.tokens[this.index - 1];
-    },
-
-    matchToken: function(type, index) {
-      var token = this.tokens[this.index + index];
-      return (token === undefined && type === '') ||
-             token && token.type === type;
-    },
-
-    match: function(token1, token2) {
-      return this.matchToken(token1, 0) &&
-        (!token2 || this.matchToken(token2, 1));
-    },
-
-  };
-
-  return Parser;
-});

+ 258 - 0
public/app/plugins/datasource/graphite/parser.ts

@@ -0,0 +1,258 @@
+
+import {Lexer} from './lexer';
+
+export function Parser(expression) {
+  this.expression = expression;
+  this.lexer = new Lexer(expression);
+  this.tokens = this.lexer.tokenize();
+  this.index = 0;
+}
+
+Parser.prototype = {
+
+  getAst: function () {
+    return this.start();
+  },
+
+  start: function () {
+    try {
+      return this.functionCall() || this.metricExpression();
+    } catch (e) {
+      return {
+        type: 'error',
+        message: e.message,
+        pos: e.pos
+      };
+    }
+  },
+
+  curlyBraceSegment: function() {
+    if (this.match('identifier', '{') || this.match('{')) {
+
+      var curlySegment = "";
+
+      while (!this.match('') && !this.match('}')) {
+        curlySegment += this.consumeToken().value;
+      }
+
+      if (!this.match('}')) {
+        this.errorMark("Expected closing '}'");
+      }
+
+      curlySegment += this.consumeToken().value;
+
+      // if curly segment is directly followed by identifier
+      // include it in the segment
+      if (this.match('identifier')) {
+        curlySegment += this.consumeToken().value;
+      }
+
+      return {
+        type: 'segment',
+        value: curlySegment
+      };
+    } else {
+      return null;
+    }
+  },
+
+  metricSegment: function() {
+    var curly = this.curlyBraceSegment();
+    if (curly) {
+      return curly;
+    }
+
+    if (this.match('identifier') || this.match('number')) {
+      // hack to handle float numbers in metric segments
+      var parts = this.consumeToken().value.split('.');
+      if (parts.length === 2) {
+        this.tokens.splice(this.index, 0, { type: '.' });
+        this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
+      }
+
+      return {
+        type: 'segment',
+        value: parts[0]
+      };
+    }
+
+    if (!this.match('templateStart')) {
+      this.errorMark('Expected metric identifier');
+    }
+
+    this.consumeToken();
+
+    if (!this.match('identifier')) {
+      this.errorMark('Expected identifier after templateStart');
+    }
+
+    var node = {
+      type: 'template',
+      value: this.consumeToken().value
+    };
+
+    if (!this.match('templateEnd')) {
+      this.errorMark('Expected templateEnd');
+    }
+
+    this.consumeToken();
+    return node;
+  },
+
+  metricExpression: function() {
+    if (!this.match('templateStart') &&
+        !this.match('identifier') &&
+          !this.match('number') &&
+            !this.match('{')) {
+      return null;
+    }
+
+    var node = {
+      type: 'metric',
+      segments: []
+    };
+
+    node.segments.push(this.metricSegment());
+
+    while (this.match('.')) {
+      this.consumeToken();
+
+      var segment = this.metricSegment();
+      if (!segment) {
+        this.errorMark('Expected metric identifier');
+      }
+
+      node.segments.push(segment);
+    }
+
+    return node;
+  },
+
+  functionCall: function() {
+    if (!this.match('identifier', '(')) {
+      return null;
+    }
+
+    var node: any = {
+      type: 'function',
+      name: this.consumeToken().value,
+    };
+
+    // consume left parenthesis
+    this.consumeToken();
+
+    node.params = this.functionParameters();
+
+    if (!this.match(')')) {
+      this.errorMark('Expected closing parenthesis');
+    }
+
+    this.consumeToken();
+
+    return node;
+  },
+
+  boolExpression: function() {
+    if (!this.match('bool')) {
+      return null;
+    }
+
+    return {
+      type: 'bool',
+      value: this.consumeToken().value === 'true',
+    };
+  },
+
+  functionParameters: function () {
+    if (this.match(')') || this.match('')) {
+      return [];
+    }
+
+    var param =
+      this.functionCall() ||
+      this.numericLiteral() ||
+      this.seriesRefExpression() ||
+      this.boolExpression() ||
+      this.metricExpression() ||
+      this.stringLiteral();
+
+    if (!this.match(',')) {
+      return [param];
+    }
+
+    this.consumeToken();
+    return [param].concat(this.functionParameters());
+  },
+
+  seriesRefExpression: function() {
+    if (!this.match('identifier')) {
+      return null;
+    }
+
+    var value = this.tokens[this.index].value;
+    if (!value.match(/\#[A-Z]/)) {
+      return null;
+    }
+
+    var token = this.consumeToken();
+
+    return {
+      type: 'series-ref',
+      value: token.value
+    };
+  },
+
+  numericLiteral: function () {
+    if (!this.match('number')) {
+      return null;
+    }
+
+    return {
+      type: 'number',
+      value: parseFloat(this.consumeToken().value)
+    };
+  },
+
+  stringLiteral: function () {
+    if (!this.match('string')) {
+      return null;
+    }
+
+    var token = this.consumeToken();
+    if (token.isUnclosed) {
+      throw { message: 'Unclosed string parameter', pos: token.pos };
+    }
+
+    return {
+      type: 'string',
+      value: token.value
+    };
+  },
+
+  errorMark: function(text) {
+    var currentToken = this.tokens[this.index];
+    var type = currentToken ? currentToken.type : 'end of string';
+    throw {
+      message: text + " instead found " + type,
+      pos: currentToken ? currentToken.pos : this.lexer.char
+    };
+  },
+
+  // returns token value and incre
+  consumeToken: function() {
+    this.index++;
+    return this.tokens[this.index - 1];
+  },
+
+  matchToken: function(type, index) {
+    var token = this.tokens[this.index + index];
+    return (token === undefined && type === '') ||
+      token && token.type === type;
+  },
+
+  match: function(token1, token2) {
+    return this.matchToken(token1, 0) &&
+      (!token2 || this.matchToken(token2, 1));
+  },
+};
+

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

@@ -1,15 +1,15 @@
 <div class="tight-form">
 	<ul class="tight-form-list pull-right">
-		<li ng-show="parserError" class="tight-form-item">
-			<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
+		<li ng-show="ctrl.parserError" class="tight-form-item">
+			<a bs-tooltip="ctrl.parserError" style="color: rgb(229, 189, 28)" role="menuitem">
 				<i class="fa fa-warning"></i>
 			</a>
 		</li>
-		<li class="tight-form-item small" ng-show="target.datasource">
-			<em>{{target.datasource}}</em>
+		<li class="tight-form-item small" ng-show="ctrl.target.datasource">
+			<em>{{ctrl.target.datasource}}</em>
 		</li>
 		<li class="tight-form-item">
-			<a class="pointer" tabindex="1" ng-click="toggleEditorMode()">
+			<a class="pointer" tabindex="1" ng-click="ctrl.toggleEditorMode()">
 				<i class="fa fa-pencil"></i>
 			</a>
 		</li>
@@ -20,24 +20,24 @@
 				</a>
 				<ul class="dropdown-menu pull-right" role="menu">
 					<li role="menuitem">
-						<a tabindex="1" ng-click="toggleEditorMode()">
+						<a tabindex="1" ng-click="ctrl.toggleEditorMode()">
 							Switch editor mode
 						</a>
 					</li>
 					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.duplicateDataQuery(target)">Duplicate</a>
+						<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
 					</li>
 					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index-1)">Move up</a>
+						<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
 					</li>
 					<li role="menuitem">
-						<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index+1)">Move down</a>
+						<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
 					</li>
 				</ul>
 			</div>
 		</li>
 		<li class="tight-form-item last">
-			<a class="pointer" tabindex="1" ng-click="ctrl.removeDataQuery(target)">
+			<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
 				<i class="fa fa-remove"></i>
 			</a>
 		</li>
@@ -45,24 +45,24 @@
 
 	<ul class="tight-form-list">
 		<li class="tight-form-item" style="min-width: 15px; text-align: center">
-			{{target.refId}}
+			{{ctrl.target.refId}}
 		</li>
 		<li>
-			<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
+			<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
 				<i class="fa fa-eye"></i>
 			</a>
 		</li>
 	</ul>
 
 	<span style="display: block; overflow: hidden;">
-		<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="target.target" give-focus="target.textEditor" spellcheck='false' ng-model-onblur ng-change="panelCtrl.getData()" ng-show="target.textEditor"></input>
+		<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="ctrl.target.target" give-focus="ctrl.target.textEditor" spellcheck='false' ng-model-onblur ng-change="ctrl.refresh()" ng-show="ctrl.target.textEditor"></input>
 	</span>
 
-	<ul class="tight-form-list" role="menu" ng-hide="target.textEditor">
-		<li ng-repeat="segment in segments" role="menuitem">
-			<metric-segment segment="segment" get-options="getAltSegments($index)" on-change="segmentValueChanged(segment, $index)"></metric-segment>
+	<ul class="tight-form-list" role="menu" ng-hide="ctrl.target.textEditor">
+		<li ng-repeat="segment in ctrl.segments" role="menuitem">
+			<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
 		</li>
-		<li ng-repeat="func in functions">
+		<li ng-repeat="func in ctrl.functions">
 			<span graphite-func-editor class="tight-form-item tight-form-func">
 			</span>
 		</li>

+ 0 - 292
public/app/plugins/datasource/graphite/query_ctrl.js

@@ -1,292 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'app/core/config',
-  './gfunc',
-  './parser'
-],
-function (angular, _, config, gfunc, Parser) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  module.controller('GraphiteQueryCtrl', function($scope, uiSegmentSrv, templateSrv) {
-    var panelCtrl = $scope.panelCtrl = $scope.ctrl;
-    var datasource = $scope.datasource;
-
-    $scope.init = function() {
-      if ($scope.target) {
-        $scope.target.target = $scope.target.target || '';
-        parseTarget();
-      }
-    };
-
-    $scope.toggleEditorMode = function() {
-      $scope.target.textEditor = !$scope.target.textEditor;
-      parseTarget();
-    };
-
-    // The way parsing and the target editor works needs
-    // to be rewritten to handle functions that take multiple series
-    function parseTarget() {
-      $scope.functions = [];
-      $scope.segments = [];
-      delete $scope.parserError;
-
-      if ($scope.target.textEditor) {
-        return;
-      }
-
-      var parser = new Parser($scope.target.target);
-      var astNode = parser.getAst();
-      if (astNode === null) {
-        checkOtherSegments(0);
-        return;
-      }
-
-      if (astNode.type === 'error') {
-        $scope.parserError = astNode.message + " at position: " + astNode.pos;
-        $scope.target.textEditor = true;
-        return;
-      }
-
-      try {
-        parseTargeRecursive(astNode);
-      }
-      catch (err) {
-        console.log('error parsing target:', err.message);
-        $scope.parserError = err.message;
-        $scope.target.textEditor = true;
-      }
-
-      checkOtherSegments($scope.segments.length - 1);
-    }
-
-    function addFunctionParameter(func, value, index, shiftBack) {
-      if (shiftBack) {
-        index = Math.max(index - 1, 0);
-      }
-      func.params[index] = value;
-    }
-
-    function parseTargeRecursive(astNode, func, index) {
-      if (astNode === null) {
-        return null;
-      }
-
-      switch(astNode.type) {
-      case 'function':
-        var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
-
-        _.each(astNode.params, function(param, index) {
-          parseTargeRecursive(param, innerFunc, index);
-        });
-
-        innerFunc.updateText();
-        $scope.functions.push(innerFunc);
-        break;
-
-      case 'series-ref':
-        addFunctionParameter(func, astNode.value, index, $scope.segments.length > 0);
-        break;
-      case 'bool':
-      case 'string':
-      case 'number':
-        if ((index-1) >= func.def.params.length) {
-          throw { message: 'invalid number of parameters to method ' + func.def.name };
-        }
-        addFunctionParameter(func, astNode.value, index, true);
-        break;
-      case 'metric':
-        if ($scope.segments.length > 0) {
-          if (astNode.segments.length !== 1) {
-            throw { message: 'Multiple metric params not supported, use text editor.' };
-          }
-          addFunctionParameter(func, astNode.segments[0].value, index, true);
-          break;
-        }
-
-        $scope.segments = _.map(astNode.segments, function(segment) {
-          return uiSegmentSrv.newSegment(segment);
-        });
-      }
-    }
-
-    function getSegmentPathUpTo(index) {
-      var arr = $scope.segments.slice(0, index);
-
-      return _.reduce(arr, function(result, segment) {
-        return result ? (result + "." + segment.value) : segment.value;
-      }, "");
-    }
-
-    function checkOtherSegments(fromIndex) {
-      if (fromIndex === 0) {
-        $scope.segments.push(uiSegmentSrv.newSelectMetric());
-        return;
-      }
-
-      var path = getSegmentPathUpTo(fromIndex + 1);
-      return datasource.metricFindQuery(path)
-        .then(function(segments) {
-          if (segments.length === 0) {
-            if (path !== '') {
-              $scope.segments = $scope.segments.splice(0, fromIndex);
-              $scope.segments.push(uiSegmentSrv.newSelectMetric());
-            }
-          } else if (segments[0].expandable) {
-            if ($scope.segments.length === fromIndex) {
-              $scope.segments.push(uiSegmentSrv.newSelectMetric());
-            }
-            else {
-              return checkOtherSegments(fromIndex + 1);
-            }
-          }
-        })
-        .then(null, function(err) {
-          $scope.parserError = err.message || 'Failed to issue metric query';
-        });
-    }
-
-    function setSegmentFocus(segmentIndex) {
-      _.each($scope.segments, function(segment, index) {
-        segment.focus = segmentIndex === index;
-      });
-    }
-
-    function wrapFunction(target, func) {
-      return func.render(target);
-    }
-
-    $scope.getAltSegments = function (index) {
-      var query = index === 0 ?  '*' : getSegmentPathUpTo(index) + '.*';
-
-      return datasource.metricFindQuery(query).then(function(segments) {
-        var altSegments = _.map(segments, function(segment) {
-          return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
-        });
-
-        if (altSegments.length === 0) { return altSegments; }
-
-        // add template variables
-        _.each(templateSrv.variables, function(variable) {
-          altSegments.unshift(uiSegmentSrv.newSegment({
-            type: 'template',
-            value: '$' + variable.name,
-            expandable: true,
-          }));
-        });
-
-        // add wildcard option
-        altSegments.unshift(uiSegmentSrv.newSegment('*'));
-        return altSegments;
-      })
-      .then(null, function(err) {
-        $scope.parserError = err.message || 'Failed to issue metric query';
-        return [];
-      });
-    };
-
-    $scope.segmentValueChanged = function (segment, segmentIndex) {
-      delete $scope.parserError;
-
-      if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
-        $scope.functions = [];
-      }
-
-      if (segment.expandable) {
-        return checkOtherSegments(segmentIndex + 1).then(function() {
-          setSegmentFocus(segmentIndex + 1);
-          $scope.targetChanged();
-        });
-      }
-      else {
-        $scope.segments = $scope.segments.splice(0, segmentIndex + 1);
-      }
-
-      setSegmentFocus(segmentIndex + 1);
-      $scope.targetChanged();
-    };
-
-    $scope.targetTextChanged = function() {
-      parseTarget();
-      panelCtrl.refresh();
-    };
-
-    $scope.targetChanged = function() {
-      if ($scope.parserError) {
-        return;
-      }
-
-      var oldTarget = $scope.target.target;
-      var target = getSegmentPathUpTo($scope.segments.length);
-      $scope.target.target = _.reduce($scope.functions, wrapFunction, target);
-
-      if ($scope.target.target !== oldTarget) {
-        if ($scope.segments[$scope.segments.length - 1].value !== 'select metric') {
-          panelCtrl.refresh();
-        }
-      }
-    };
-
-    $scope.removeFunction = function(func) {
-      $scope.functions = _.without($scope.functions, func);
-      $scope.targetChanged();
-    };
-
-    $scope.addFunction = function(funcDef) {
-      var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
-      newFunc.added = true;
-      $scope.functions.push(newFunc);
-
-      $scope.moveAliasFuncLast();
-      $scope.smartlyHandleNewAliasByNode(newFunc);
-
-      if ($scope.segments.length === 1 && $scope.segments[0].fake) {
-        $scope.segments = [];
-      }
-
-      if (!newFunc.params.length && newFunc.added) {
-        $scope.targetChanged();
-      }
-    };
-
-    $scope.moveAliasFuncLast = function() {
-      var aliasFunc = _.find($scope.functions, function(func) {
-        return func.def.name === 'alias' ||
-          func.def.name === 'aliasByNode' ||
-          func.def.name === 'aliasByMetric';
-      });
-
-      if (aliasFunc) {
-        $scope.functions = _.without($scope.functions, aliasFunc);
-        $scope.functions.push(aliasFunc);
-      }
-    };
-
-    $scope.smartlyHandleNewAliasByNode = function(func) {
-      if (func.def.name !== 'aliasByNode') {
-        return;
-      }
-      for(var i = 0; i < $scope.segments.length; i++) {
-        if ($scope.segments[i].value.indexOf('*') >= 0)  {
-          func.params[0] = i;
-          func.added = false;
-          $scope.targetChanged();
-          return;
-        }
-      }
-    };
-
-    $scope.toggleMetricOptions = function() {
-      $scope.panel.metricOptionsEnabled = !$scope.panel.metricOptionsEnabled;
-      if (!$scope.panel.metricOptionsEnabled) {
-        delete $scope.panel.cacheTimeout;
-      }
-    };
-
-    $scope.init();
-
-  });
-
-});

+ 275 - 0
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -0,0 +1,275 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import './add_graphite_func';
+
+import angular from 'angular';
+import _ from 'lodash';
+import moment from 'moment';
+import gfunc from './gfunc';
+import {Parser} from './parser';
+import {QueryCtrl} from 'app/features/panel/panel';
+
+export class GraphiteQueryCtrl extends QueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/graphite/partials/query.editor.html';
+
+  functions: any[];
+  segments: any[];
+  parserError: string;
+
+  constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
+    super($scope, $injector);
+
+    if (this.target) {
+      this.target.target = this.target.target || '';
+      this.parseTarget();
+    }
+  }
+
+  toggleEditorMode() {
+    this.target.textEditor = !this.target.textEditor;
+    this.parseTarget();
+  }
+
+  parseTarget() {
+    this.functions = [];
+    this.segments = [];
+    delete this.parserError;
+
+    if (this.target.textEditor) {
+      return;
+    }
+
+    var parser = new Parser(this.target.target);
+    var astNode = parser.getAst();
+    if (astNode === null) {
+      this.checkOtherSegments(0);
+      return;
+    }
+
+    if (astNode.type === 'error') {
+      this.parserError = astNode.message + " at position: " + astNode.pos;
+      this.target.textEditor = true;
+      return;
+    }
+
+    try {
+      this.parseTargeRecursive(astNode, null, 0);
+    } catch (err) {
+      console.log('error parsing target:', err.message);
+      this.parserError = err.message;
+      this.target.textEditor = true;
+    }
+
+    this.checkOtherSegments(this.segments.length - 1);
+  }
+
+  addFunctionParameter(func, value, index, shiftBack) {
+    if (shiftBack) {
+      index = Math.max(index - 1, 0);
+    }
+    func.params[index] = value;
+  }
+
+  parseTargeRecursive(astNode, func, index) {
+    if (astNode === null) {
+      return null;
+    }
+
+    switch (astNode.type) {
+      case 'function':
+        var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
+        _.each(astNode.params, (param, index) => {
+          this.parseTargeRecursive(param, innerFunc, index);
+        });
+
+        innerFunc.updateText();
+        this.functions.push(innerFunc);
+        break;
+      case 'series-ref':
+        this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
+        break;
+      case 'bool':
+      case 'string':
+      case 'number':
+        if ((index-1) >= func.def.params.length) {
+          throw { message: 'invalid number of parameters to method ' + func.def.name };
+        }
+        this.addFunctionParameter(func, astNode.value, index, true);
+      break;
+      case 'metric':
+        if (this.segments.length > 0) {
+        if (astNode.segments.length !== 1) {
+          throw { message: 'Multiple metric params not supported, use text editor.' };
+        }
+        this.addFunctionParameter(func, astNode.segments[0].value, index, true);
+        break;
+      }
+
+      this.segments = _.map(astNode.segments, segment => {
+        return this.uiSegmentSrv.newSegment(segment);
+      });
+    }
+  }
+
+  getSegmentPathUpTo(index) {
+    var arr = this.segments.slice(0, index);
+
+    return _.reduce(arr, function(result, segment) {
+      return result ? (result + "." + segment.value) : segment.value;
+    }, "");
+  }
+
+  checkOtherSegments(fromIndex) {
+    if (fromIndex === 0) {
+      this.segments.push(this.uiSegmentSrv.newSelectMetric());
+      return;
+    }
+
+    var path = this.getSegmentPathUpTo(fromIndex + 1);
+    return this.datasource.metricFindQuery(path).then(segments => {
+      if (segments.length === 0) {
+        if (path !== '') {
+          this.segments = this.segments.splice(0, fromIndex);
+          this.segments.push(this.uiSegmentSrv.newSelectMetric());
+        }
+      } else if (segments[0].expandable) {
+        if (this.segments.length === fromIndex) {
+          this.segments.push(this.uiSegmentSrv.newSelectMetric());
+        } else {
+          return this.checkOtherSegments(fromIndex + 1);
+        }
+      }
+    }).catch(err => {
+      this.parserError = err.message || 'Failed to issue metric query';
+    });
+  }
+
+  setSegmentFocus(segmentIndex) {
+    _.each(this.segments, (segment, index) => {
+      segment.focus = segmentIndex === index;
+    });
+  }
+
+  wrapFunction(target, func) {
+    return func.render(target);
+  }
+
+  getAltSegments(index) {
+    var query = index === 0 ?  '*' : this.getSegmentPathUpTo(index) + '.*';
+
+    return this.datasource.metricFindQuery(query).then(segments => {
+      var altSegments = _.map(segments, segment => {
+        return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
+      });
+
+      if (altSegments.length === 0) { return altSegments; }
+
+      // add template variables
+      _.each(this.templateSrv.variables, variable => {
+        altSegments.unshift(this.uiSegmentSrv.newSegment({
+          type: 'template',
+          value: '$' + variable.name,
+          expandable: true,
+        }));
+      });
+
+      // add wildcard option
+      altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
+      return altSegments;
+    }).catch(err => {
+      this.parserError = err.message || 'Failed to issue metric query';
+      return [];
+    });
+  }
+
+  segmentValueChanged(segment, segmentIndex) {
+    delete this.parserError;
+
+    if (this.functions.length > 0 && this.functions[0].def.fake) {
+      this.functions = [];
+    }
+
+    if (segment.expandable) {
+      return this.checkOtherSegments(segmentIndex + 1).then(() => {
+        this.setSegmentFocus(segmentIndex + 1);
+        this.targetChanged();
+      });
+    } else {
+      this.segments = this.segments.splice(0, segmentIndex + 1);
+    }
+
+    this.setSegmentFocus(segmentIndex + 1);
+    this.targetChanged();
+  }
+
+  targetTextChanged() {
+    this.parseTarget();
+    this.panelCtrl.refresh();
+  }
+
+  targetChanged() {
+    if (this.parserError) {
+      return;
+    }
+
+    var oldTarget = this.target.target;
+    var target = this.getSegmentPathUpTo(this.segments.length);
+    this.target.target = _.reduce(this.functions, this.wrapFunction, target);
+
+    if (this.target.target !== oldTarget) {
+      if (this.segments[this.segments.length - 1].value !== 'select metric') {
+        this.panelCtrl.refresh();
+      }
+    }
+  }
+
+  removeFunction(func) {
+    this.functions = _.without(this.functions, func);
+    this.targetChanged();
+  }
+
+  addFunction(funcDef) {
+    var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
+    newFunc.added = true;
+    this.functions.push(newFunc);
+
+    this.moveAliasFuncLast();
+    this.smartlyHandleNewAliasByNode(newFunc);
+
+    if (this.segments.length === 1 && this.segments[0].fake) {
+      this.segments = [];
+    }
+
+    if (!newFunc.params.length && newFunc.added) {
+      this.targetChanged();
+    }
+  }
+
+  moveAliasFuncLast() {
+    var aliasFunc = _.find(this.functions, function(func) {
+      return func.def.name === 'alias' ||
+        func.def.name === 'aliasByNode' ||
+        func.def.name === 'aliasByMetric';
+    });
+
+    if (aliasFunc) {
+      this.functions = _.without(this.functions, aliasFunc);
+      this.functions.push(aliasFunc);
+    }
+  }
+
+  smartlyHandleNewAliasByNode(func) {
+    if (func.def.name !== 'aliasByNode') {
+      return;
+    }
+
+    for (var i = 0; i < this.segments.length; i++) {
+      if (this.segments[i].value.indexOf('*') >= 0)  {
+        func.params[0] = i;
+        func.added = false;
+        this.targetChanged();
+        return;
+      }
+    }
+  }
+}

+ 2 - 2
public/app/plugins/datasource/graphite/specs/datasource_specs.ts

@@ -1,7 +1,7 @@
 
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import helpers from 'test/specs/helpers';
-import Datasource from "../datasource";
+import {GraphiteDatasource} from "../datasource";
 
 describe('graphiteDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
@@ -18,7 +18,7 @@ describe('graphiteDatasource', function() {
   }));
 
   beforeEach(function() {
-    ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+    ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings});
   });
 
   describe('When querying influxdb with one target using query editor target spec', function() {

+ 118 - 0
public/app/plugins/datasource/graphite/specs/lexer_specs.ts

@@ -0,0 +1,118 @@
+
+import {describe, it, expect} from 'test/lib/common';
+import {Lexer} from '../lexer';
+
+describe('when lexing graphite expression', function() {
+
+  it('should tokenize metric expression', function() {
+    var lexer = new Lexer('metric.test.*.asd.count');
+    var tokens = lexer.tokenize();
+    expect(tokens[0].value).to.be('metric');
+    expect(tokens[1].value).to.be('.');
+    expect(tokens[2].type).to.be('identifier');
+    expect(tokens[4].type).to.be('identifier');
+    expect(tokens[4].pos).to.be(13);
+  });
+
+  it('should tokenize metric expression with dash', function() {
+    var lexer = new Lexer('metric.test.se1-server-*.asd.count');
+    var tokens = lexer.tokenize();
+    expect(tokens[4].type).to.be('identifier');
+    expect(tokens[4].value).to.be('se1-server-*');
+  });
+
+  it('should tokenize metric expression with dash2', function() {
+    var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*');
+    var tokens = lexer.tokenize();
+    expect(tokens[0].value).to.be('net');
+    expect(tokens[2].value).to.be('192-168-1-1');
+  });
+
+  it('should tokenize metric expression with equal sign', function() {
+    var lexer = new Lexer('apps=test');
+    var tokens = lexer.tokenize();
+    expect(tokens[0].value).to.be('apps=test');
+  });
+
+  it('simple function2', function() {
+    var lexer = new Lexer('offset(test.metric, -100)');
+    var tokens = lexer.tokenize();
+    expect(tokens[2].type).to.be('identifier');
+    expect(tokens[4].type).to.be('identifier');
+    expect(tokens[6].type).to.be('number');
+  });
+
+  it('should tokenize metric expression with curly braces', function() {
+    var lexer = new Lexer('metric.se1-{first, second}.count');
+    var tokens = lexer.tokenize();
+    expect(tokens.length).to.be(10);
+    expect(tokens[3].type).to.be('{');
+    expect(tokens[4].value).to.be('first');
+    expect(tokens[5].value).to.be(',');
+    expect(tokens[6].value).to.be('second');
+  });
+
+  it('should tokenize metric expression with number segments', function() {
+    var lexer = new Lexer("metric.10.12_10.test");
+    var tokens = lexer.tokenize();
+    expect(tokens[0].type).to.be('identifier');
+    expect(tokens[2].type).to.be('identifier');
+    expect(tokens[2].value).to.be('10');
+    expect(tokens[4].value).to.be('12_10');
+    expect(tokens[4].type).to.be('identifier');
+  });
+
+  it('should tokenize func call with numbered metric and number arg', function() {
+    var lexer = new Lexer("scale(metric.10, 15)");
+    var tokens = lexer.tokenize();
+    expect(tokens[0].type).to.be('identifier');
+    expect(tokens[2].type).to.be('identifier');
+    expect(tokens[2].value).to.be('metric');
+    expect(tokens[4].value).to.be('10');
+    expect(tokens[4].type).to.be('number');
+    expect(tokens[6].type).to.be('number');
+  });
+
+  it('should tokenize metric with template parameter', function() {
+    var lexer = new Lexer("metric.[[server]].test");
+    var tokens = lexer.tokenize();
+    expect(tokens[2].type).to.be('identifier');
+    expect(tokens[2].value).to.be('[[server]]');
+    expect(tokens[4].type).to.be('identifier');
+  });
+
+  it('should tokenize metric with question mark', function() {
+    var lexer = new Lexer("metric.server_??.test");
+    var tokens = lexer.tokenize();
+    expect(tokens[2].type).to.be('identifier');
+    expect(tokens[2].value).to.be('server_??');
+    expect(tokens[4].type).to.be('identifier');
+  });
+
+  it('should handle error with unterminated string', function() {
+    var lexer = new Lexer("alias(metric, 'asd)");
+    var tokens = lexer.tokenize();
+    expect(tokens[0].value).to.be('alias');
+    expect(tokens[1].value).to.be('(');
+    expect(tokens[2].value).to.be('metric');
+    expect(tokens[3].value).to.be(',');
+    expect(tokens[4].type).to.be('string');
+    expect(tokens[4].isUnclosed).to.be(true);
+    expect(tokens[4].pos).to.be(20);
+  });
+
+  it('should handle float parameters', function() {
+    var lexer = new Lexer("alias(metric, 0.002)");
+    var tokens = lexer.tokenize();
+    expect(tokens[4].type).to.be('number');
+    expect(tokens[4].value).to.be('0.002');
+  });
+
+  it('should handle bool parameters', function() {
+    var lexer = new Lexer("alias(metric, true, false)");
+    var tokens = lexer.tokenize();
+    expect(tokens[4].type).to.be('bool');
+    expect(tokens[4].value).to.be('true');
+    expect(tokens[6].type).to.be('bool');
+  });
+});

+ 183 - 0
public/app/plugins/datasource/graphite/specs/parser_specs.ts

@@ -0,0 +1,183 @@
+import {describe, it, expect} from 'test/lib/common';
+import {Parser} from '../parser';
+
+describe('when parsing', function() {
+
+  it('simple metric expression', function() {
+    var parser = new Parser('metric.test.*.asd.count');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).to.be('metric');
+    expect(rootNode.segments.length).to.be(5);
+    expect(rootNode.segments[0].value).to.be('metric');
+  });
+
+  it('simple metric expression with numbers in segments', function() {
+    var parser = new Parser('metric.10.15_20.5');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).to.be('metric');
+    expect(rootNode.segments.length).to.be(4);
+    expect(rootNode.segments[1].value).to.be('10');
+    expect(rootNode.segments[2].value).to.be('15_20');
+    expect(rootNode.segments[3].value).to.be('5');
+  });
+
+  it('simple metric expression with curly braces', function() {
+    var parser = new Parser('metric.se1-{count, max}');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).to.be('metric');
+    expect(rootNode.segments.length).to.be(2);
+    expect(rootNode.segments[1].value).to.be('se1-{count,max}');
+  });
+
+  it('simple metric expression with curly braces at start of segment and with post chars', function() {
+    var parser = new Parser('metric.{count, max}-something.count');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).to.be('metric');
+    expect(rootNode.segments.length).to.be(3);
+    expect(rootNode.segments[1].value).to.be('{count,max}-something');
+  });
+
+  it('simple function', function() {
+    var parser = new Parser('sum(test)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params.length).to.be(1);
+  });
+
+  it('simple function2', function() {
+    var parser = new Parser('offset(test.metric, -100)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params[0].type).to.be('metric');
+    expect(rootNode.params[1].type).to.be('number');
+  });
+
+  it('simple function with string arg', function() {
+    var parser = new Parser("randomWalk('test')");
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params.length).to.be(1);
+    expect(rootNode.params[0].type).to.be('string');
+  });
+
+  it('function with multiple args', function() {
+    var parser = new Parser("sum(test, 1, 'test')");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params.length).to.be(3);
+    expect(rootNode.params[0].type).to.be('metric');
+    expect(rootNode.params[1].type).to.be('number');
+    expect(rootNode.params[2].type).to.be('string');
+  });
+
+  it('function with nested function', function() {
+    var parser = new Parser("sum(scaleToSeconds(test, 1))");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params.length).to.be(1);
+    expect(rootNode.params[0].type).to.be('function');
+    expect(rootNode.params[0].name).to.be('scaleToSeconds');
+    expect(rootNode.params[0].params.length).to.be(2);
+    expect(rootNode.params[0].params[0].type).to.be('metric');
+    expect(rootNode.params[0].params[1].type).to.be('number');
+  });
+
+  it('function with multiple series', function() {
+    var parser = new Parser("sum(test.test.*.count, test.timers.*.count)");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params.length).to.be(2);
+    expect(rootNode.params[0].type).to.be('metric');
+    expect(rootNode.params[1].type).to.be('metric');
+  });
+
+  it('function with templated series', function() {
+    var parser = new Parser("sum(test.[[server]].count)");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).to.be(undefined);
+    expect(rootNode.params[0].type).to.be('metric');
+    expect(rootNode.params[0].segments[1].type).to.be('segment');
+    expect(rootNode.params[0].segments[1].value).to.be('[[server]]');
+  });
+
+  it('invalid metric expression', function() {
+    var parser = new Parser('metric.test.*.asd.');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).to.be('Expected metric identifier instead found end of string');
+    expect(rootNode.pos).to.be(19);
+  });
+
+  it('invalid function expression missing closing parenthesis', function() {
+    var parser = new Parser('sum(test');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).to.be('Expected closing parenthesis instead found end of string');
+    expect(rootNode.pos).to.be(9);
+  });
+
+  it('unclosed string in function', function() {
+    var parser = new Parser("sum('test)");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).to.be('Unclosed string parameter');
+    expect(rootNode.pos).to.be(11);
+  });
+
+  it('handle issue #69', function() {
+    var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('function');
+  });
+
+  it('handle float function arguments', function() {
+    var parser = new Parser('scale(test, 0.002)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params[1].type).to.be('number');
+    expect(rootNode.params[1].value).to.be(0.002);
+  });
+
+  it('handle curly brace pattern at start', function() {
+    var parser = new Parser('{apps}.test');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('metric');
+    expect(rootNode.segments[0].value).to.be('{apps}');
+    expect(rootNode.segments[1].value).to.be('test');
+  });
+
+  it('series parameters', function() {
+    var parser = new Parser('asPercent(#A, #B)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params[0].type).to.be('series-ref');
+    expect(rootNode.params[0].value).to.be('#A');
+    expect(rootNode.params[1].value).to.be('#B');
+  });
+
+  it('series parameters, issue 2788', function() {
+    var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)");
+    var rootNode = parser.getAst();
+    expect(rootNode.type).to.be('function');
+    expect(rootNode.params[0].type).to.be('function');
+    expect(rootNode.params[1].value).to.be('10m');
+    expect(rootNode.params[3].type).to.be('bool');
+  });
+
+  it('should parse metric expression with ip number segments', function() {
+    var parser = new Parser('5.10.123.5');
+    var rootNode = parser.getAst();
+    expect(rootNode.segments[0].value).to.be('5');
+    expect(rootNode.segments[1].value).to.be('10');
+    expect(rootNode.segments[2].value).to.be('123');
+    expect(rootNode.segments[3].value).to.be('5');
+  });
+});

+ 50 - 65
public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts

@@ -5,6 +5,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co
 
 import gfunc from '../gfunc';
 import helpers from 'test/specs/helpers';
+import {GraphiteQueryCtrl} from '../query_ctrl';
 
 describe('GraphiteQueryCtrl', function() {
   var ctx = new helpers.ControllerTestContext();
@@ -17,53 +18,47 @@ describe('GraphiteQueryCtrl', function() {
   beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
     ctx.$q = $q;
     ctx.scope = $rootScope.$new();
-    ctx.scope.ctrl = {panel: ctx.panel};
-    ctx.panelCtrl = ctx.scope.ctrl;
-    ctx.scope.datasource = ctx.datasource;
-    ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-    ctx.controller = $controller('GraphiteQueryCtrl', {$scope: ctx.scope});
-  }));
-
-  beforeEach(function() {
-    ctx.scope.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.panelCtrl = {panel: {}};
+    ctx.panelCtrl.refresh = sinon.spy();
 
-  describe('init', function() {
-    beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.$digest();
+    ctx.ctrl = $controller(GraphiteQueryCtrl, {$scope: ctx.scope}, {
+      panelCtrl: ctx.panelCtrl,
+      datasource: ctx.datasource,
+      target: ctx.target
     });
+    ctx.scope.$digest();
+  }));
 
+  describe('init', function() {
     it('should validate metric key exists', function() {
-      expect(ctx.scope.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*');
+      expect(ctx.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*');
     });
 
     it('should delete last segment if no metrics are found', function() {
-      expect(ctx.scope.segments[2].value).to.be('select metric');
+      expect(ctx.ctrl.segments[2].value).to.be('select metric');
     });
 
     it('should parse expression and build function model', function() {
-      expect(ctx.scope.functions.length).to.be(2);
+      expect(ctx.ctrl.functions.length).to.be(2);
     });
   });
 
   describe('when adding function', function() {
     beforeEach(function() {
-      ctx.scope.target.target = 'test.prod.*.count';
-      ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}]));
-      ctx.scope.init();
-      ctx.scope.$digest();
-
-      ctx.panelCtrl.refresh = sinon.spy();
-      ctx.scope.addFunction(gfunc.getFuncDef('aliasByNode'));
+      ctx.ctrl.target.target = 'test.prod.*.count';
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
     });
 
     it('should add function with correct node number', function() {
-      expect(ctx.scope.functions[0].params[0]).to.be(2);
+      expect(ctx.ctrl.functions[0].params[0]).to.be(2);
     });
 
     it('should update target', function() {
-      expect(ctx.scope.target.target).to.be('aliasByNode(test.prod.*.count, 2)');
+      expect(ctx.ctrl.target.target).to.be('aliasByNode(test.prod.*.count, 2)');
     });
 
     it('should call refresh', function() {
@@ -73,78 +68,72 @@ describe('GraphiteQueryCtrl', function() {
 
   describe('when adding function before any metric segment', function() {
     beforeEach(function() {
-      ctx.scope.target.target = '';
-      ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}]));
-      ctx.scope.init();
-      ctx.scope.$digest();
-      ctx.scope.addFunction(gfunc.getFuncDef('asPercent'));
+      ctx.ctrl.target.target = '';
+      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
     });
 
     it('should add function and remove select metric link', function() {
-      expect(ctx.scope.segments.length).to.be(0);
+      expect(ctx.ctrl.segments.length).to.be(0);
     });
   });
 
   describe('when initalizing target without metric expression and only function', function() {
     beforeEach(function() {
-      ctx.scope.target.target = 'asPercent(#A, #B)';
-      ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
-      ctx.scope.init();
+      ctx.ctrl.target.target = 'asPercent(#A, #B)';
+      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.parseTarget();
       ctx.scope.$digest();
     });
 
     it('should not add select metric segment', function() {
-      expect(ctx.scope.segments.length).to.be(0);
+      expect(ctx.ctrl.segments.length).to.be(0);
     });
 
     it('should add both series refs as params', function() {
-      expect(ctx.scope.functions[0].params.length).to.be(2);
+      expect(ctx.ctrl.functions[0].params.length).to.be(2);
     });
-
   });
 
   describe('when initializing a target with single param func using variable', function() {
     beforeEach(function() {
-      ctx.scope.target.target = 'movingAverage(prod.count, $var)';
-      ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
-      ctx.scope.init();
-      ctx.scope.$digest();
+      ctx.ctrl.target.target = 'movingAverage(prod.count, $var)';
+      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.parseTarget();
     });
 
     it('should add 2 segments', function() {
-      expect(ctx.scope.segments.length).to.be(2);
+      expect(ctx.ctrl.segments.length).to.be(2);
     });
 
     it('should add function param', function() {
-      expect(ctx.scope.functions[0].params.length).to.be(1);
+      expect(ctx.ctrl.functions[0].params.length).to.be(1);
     });
-
   });
 
   describe('when initalizing target without metric expression and function with series-ref', function() {
     beforeEach(function() {
-      ctx.scope.target.target = 'asPercent(metric.node.count, #A)';
-      ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
-      ctx.scope.init();
-      ctx.scope.$digest();
-      ctx.scope.$parent = { get_data: sinon.spy() };
+      ctx.ctrl.target.target = 'asPercent(metric.node.count, #A)';
+      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.parseTarget();
     });
 
     it('should add segments', function() {
-      expect(ctx.scope.segments.length).to.be(3);
+      expect(ctx.ctrl.segments.length).to.be(3);
     });
 
     it('should have correct func params', function() {
-      expect(ctx.scope.functions[0].params.length).to.be(1);
+      expect(ctx.ctrl.functions[0].params.length).to.be(1);
     });
   });
 
   describe('when getting altSegments and metricFindQuery retuns empty array', function() {
     beforeEach(function() {
-      ctx.scope.target.target = 'test.count';
-      ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([]));
-      ctx.scope.init();
-      ctx.scope.getAltSegments(1).then(function(results) {
+      ctx.ctrl.target.target = 'test.count';
+      ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.getAltSegments(1).then(function(results) {
         ctx.altSegments = results;
       });
       ctx.scope.$digest();
@@ -153,22 +142,18 @@ describe('GraphiteQueryCtrl', function() {
     it('should have no segments', function() {
       expect(ctx.altSegments.length).to.be(0);
     });
-
   });
 
   describe('targetChanged', function() {
     beforeEach(function() {
-      ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}]));
-      ctx.scope.init();
-      ctx.scope.$digest();
-
-      ctx.panelCtrl.refresh = sinon.spy();
-      ctx.scope.target.target = '';
-      ctx.scope.targetChanged();
+      ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
+      ctx.ctrl.parseTarget();
+      ctx.ctrl.target.target = '';
+      ctx.ctrl.targetChanged();
     });
 
     it('should rebuld target after expression model', function() {
-      expect(ctx.scope.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
+      expect(ctx.ctrl.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
     });
 
     it('should call panelCtrl.refresh', function() {

+ 2 - 4
public/app/plugins/datasource/prometheus/datasource.ts

@@ -9,7 +9,7 @@ import * as dateMath from 'app/core/utils/datemath';
 var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/;
 
 /** @ngInject */
-function PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv) {
   this.type = 'prometheus';
   this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
   this.name = instanceSettings.name;
@@ -271,8 +271,6 @@ function PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv) {
     if (_.isString(date)) {
       date = dateMath.parse(date, roundUp);
     }
-    return Math.floor(date.valueOf() / 1000);
+    return Math.ceil(date.valueOf() / 1000);
   }
 }
-
-export {PrometheusDatasource};

+ 1 - 12
public/app/plugins/datasource/prometheus/module.ts

@@ -1,23 +1,12 @@
 import {PrometheusDatasource} from './datasource';
 import {PrometheusQueryCtrl} from './query_ctrl';
 
-
-
-
-  // function metricsQueryEditor() {
-  //   return {controller: 'PrometheusQueryCtrl', templateUrl: 'public/app/plugins/datasource/prometheus/partials/query.editor.html'};
-  // }
-  //
-  // function configView() {
-  //   return {templateUrl: ''};
-  // }
-
 class PrometheusConfigViewCtrl {
   static templateUrl = 'public/app/plugins/datasource/prometheus/partials/config.html';
 }
 
 export {
   PrometheusDatasource as Datasource,
-  PrometheusQueryCtrl as MetricsQueryEditor,
+  PrometheusQueryCtrl as QueryCtrl,
   PrometheusConfigViewCtrl as ConfigView
 };

+ 3 - 3
public/app/plugins/datasource/prometheus/query_ctrl.ts

@@ -5,10 +5,9 @@ import _ from 'lodash';
 import moment from 'moment';
 
 import * as dateMath from 'app/core/utils/datemath';
-import {QueryEditorCtrl} from 'app/features/panel/panel';
+import {QueryCtrl} from 'app/features/panel/panel';
 
-/** @ngInject */
-class PrometheusQueryCtrl extends QueryEditorCtrl {
+class PrometheusQueryCtrl extends QueryCtrl {
   static templateUrl = 'public/app/plugins/datasource/prometheus/partials/query.editor.html';
   metric: any;
   resolutions: any;
@@ -16,6 +15,7 @@ class PrometheusQueryCtrl extends QueryEditorCtrl {
   suggestMetrics: any;
   linkToPrometheus: any;
 
+  /** @ngInject */
   constructor($scope, $injector, private templateSrv) {
     super($scope, $injector);
 

+ 2 - 2
public/app/plugins/datasource/prometheus/specs/datasource_specs.ts

@@ -1,7 +1,7 @@
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import moment from 'moment';
 import helpers from 'test/specs/helpers';
-import Datasource from '../datasource';
+import {PrometheusDatasource} from '../datasource';
 
 describe('PrometheusDatasource', function() {
   var ctx = new helpers.ServiceTestContext();
@@ -13,7 +13,7 @@ describe('PrometheusDatasource', function() {
     ctx.$q = $q;
     ctx.$httpBackend =  $httpBackend;
     ctx.$rootScope = $rootScope;
-    ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
+    ctx.ds = $injector.instantiate(PrometheusDatasource, {instanceSettings: instanceSettings});
   }));
 
   describe('When querying prometheus with one target using query editor target spec', function() {

+ 0 - 33
public/test/specs/dashboardSrv-specs.js

@@ -47,39 +47,6 @@ define([
       });
     });
 
-    describe('addDataQueryTo', function() {
-      var dashboard, panel;
-
-      beforeEach(function() {
-        panel = {targets:[]};
-        dashboard = _dashboardSrv.create({});
-        dashboard.rows.push({panels: [panel]});
-      });
-
-      it('should add target', function() {
-        dashboard.addDataQueryTo(panel);
-        expect(panel.targets.length).to.be(1);
-      });
-
-      it('should set refId', function() {
-        dashboard.addDataQueryTo(panel);
-        expect(panel.targets[0].refId).to.be('A');
-      });
-
-      it('should set refId to first available letter', function() {
-        panel.targets = [{refId: 'A'}];
-        dashboard.addDataQueryTo(panel);
-        expect(panel.targets[1].refId).to.be('B');
-      });
-
-      it('duplicate should get unique refId', function() {
-        panel.targets = [{refId: 'A'}];
-        dashboard.duplicateDataQuery(panel, panel.targets[0]);
-        expect(panel.targets[1].refId).to.be('B');
-      });
-
-    });
-
     describe('row and panel manipulation', function() {
       var dashboard;
 

+ 0 - 122
public/test/specs/lexer-specs.js

@@ -1,122 +0,0 @@
-define([
-  'app/plugins/datasource/graphite/lexer'
-], function(Lexer) {
-  'use strict';
-
-  describe('when lexing graphite expression', function() {
-
-    it('should tokenize metric expression', function() {
-      var lexer = new Lexer('metric.test.*.asd.count');
-      var tokens = lexer.tokenize();
-      expect(tokens[0].value).to.be('metric');
-      expect(tokens[1].value).to.be('.');
-      expect(tokens[2].type).to.be('identifier');
-      expect(tokens[4].type).to.be('identifier');
-      expect(tokens[4].pos).to.be(13);
-    });
-
-    it('should tokenize metric expression with dash', function() {
-      var lexer = new Lexer('metric.test.se1-server-*.asd.count');
-      var tokens = lexer.tokenize();
-      expect(tokens[4].type).to.be('identifier');
-      expect(tokens[4].value).to.be('se1-server-*');
-    });
-
-    it('should tokenize metric expression with dash2', function() {
-      var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*');
-      var tokens = lexer.tokenize();
-      expect(tokens[0].value).to.be('net');
-      expect(tokens[2].value).to.be('192-168-1-1');
-    });
-
-    it('should tokenize metric expression with equal sign', function() {
-      var lexer = new Lexer('apps=test');
-      var tokens = lexer.tokenize();
-      expect(tokens[0].value).to.be('apps=test');
-    });
-
-    it('simple function2', function() {
-      var lexer = new Lexer('offset(test.metric, -100)');
-      var tokens = lexer.tokenize();
-      expect(tokens[2].type).to.be('identifier');
-      expect(tokens[4].type).to.be('identifier');
-      expect(tokens[6].type).to.be('number');
-    });
-
-    it('should tokenize metric expression with curly braces', function() {
-      var lexer = new Lexer('metric.se1-{first, second}.count');
-      var tokens = lexer.tokenize();
-      expect(tokens.length).to.be(10);
-      expect(tokens[3].type).to.be('{');
-      expect(tokens[4].value).to.be('first');
-      expect(tokens[5].value).to.be(',');
-      expect(tokens[6].value).to.be('second');
-    });
-
-    it('should tokenize metric expression with number segments', function() {
-      var lexer = new Lexer("metric.10.12_10.test");
-      var tokens = lexer.tokenize();
-      expect(tokens[0].type).to.be('identifier');
-      expect(tokens[2].type).to.be('identifier');
-      expect(tokens[2].value).to.be('10');
-      expect(tokens[4].value).to.be('12_10');
-      expect(tokens[4].type).to.be('identifier');
-    });
-
-    it('should tokenize func call with numbered metric and number arg', function() {
-      var lexer = new Lexer("scale(metric.10, 15)");
-      var tokens = lexer.tokenize();
-      expect(tokens[0].type).to.be('identifier');
-      expect(tokens[2].type).to.be('identifier');
-      expect(tokens[2].value).to.be('metric');
-      expect(tokens[4].value).to.be('10');
-      expect(tokens[4].type).to.be('number');
-      expect(tokens[6].type).to.be('number');
-    });
-
-    it('should tokenize metric with template parameter', function() {
-      var lexer = new Lexer("metric.[[server]].test");
-      var tokens = lexer.tokenize();
-      expect(tokens[2].type).to.be('identifier');
-      expect(tokens[2].value).to.be('[[server]]');
-      expect(tokens[4].type).to.be('identifier');
-    });
-
-    it('should tokenize metric with question mark', function() {
-      var lexer = new Lexer("metric.server_??.test");
-      var tokens = lexer.tokenize();
-      expect(tokens[2].type).to.be('identifier');
-      expect(tokens[2].value).to.be('server_??');
-      expect(tokens[4].type).to.be('identifier');
-    });
-
-    it('should handle error with unterminated string', function() {
-      var lexer = new Lexer("alias(metric, 'asd)");
-      var tokens = lexer.tokenize();
-      expect(tokens[0].value).to.be('alias');
-      expect(tokens[1].value).to.be('(');
-      expect(tokens[2].value).to.be('metric');
-      expect(tokens[3].value).to.be(',');
-      expect(tokens[4].type).to.be('string');
-      expect(tokens[4].isUnclosed).to.be(true);
-      expect(tokens[4].pos).to.be(20);
-    });
-
-    it('should handle float parameters', function() {
-      var lexer = new Lexer("alias(metric, 0.002)");
-      var tokens = lexer.tokenize();
-      expect(tokens[4].type).to.be('number');
-      expect(tokens[4].value).to.be('0.002');
-    });
-
-    it('should handle bool parameters', function() {
-      var lexer = new Lexer("alias(metric, true, false)");
-      var tokens = lexer.tokenize();
-      expect(tokens[4].type).to.be('bool');
-      expect(tokens[4].value).to.be('true');
-      expect(tokens[6].type).to.be('bool');
-    });
-
-  });
-
-});

+ 0 - 188
public/test/specs/parser-specs.js

@@ -1,188 +0,0 @@
-define([
-  'app/plugins/datasource/graphite/parser'
-], function(Parser) {
-  'use strict';
-
-  describe('when parsing', function() {
-
-    it('simple metric expression', function() {
-      var parser = new Parser('metric.test.*.asd.count');
-      var rootNode = parser.getAst();
-
-      expect(rootNode.type).to.be('metric');
-      expect(rootNode.segments.length).to.be(5);
-      expect(rootNode.segments[0].value).to.be('metric');
-    });
-
-    it('simple metric expression with numbers in segments', function() {
-      var parser = new Parser('metric.10.15_20.5');
-      var rootNode = parser.getAst();
-
-      expect(rootNode.type).to.be('metric');
-      expect(rootNode.segments.length).to.be(4);
-      expect(rootNode.segments[1].value).to.be('10');
-      expect(rootNode.segments[2].value).to.be('15_20');
-      expect(rootNode.segments[3].value).to.be('5');
-    });
-
-    it('simple metric expression with curly braces', function() {
-      var parser = new Parser('metric.se1-{count, max}');
-      var rootNode = parser.getAst();
-
-      expect(rootNode.type).to.be('metric');
-      expect(rootNode.segments.length).to.be(2);
-      expect(rootNode.segments[1].value).to.be('se1-{count,max}');
-    });
-
-    it('simple metric expression with curly braces at start of segment and with post chars', function() {
-      var parser = new Parser('metric.{count, max}-something.count');
-      var rootNode = parser.getAst();
-
-      expect(rootNode.type).to.be('metric');
-      expect(rootNode.segments.length).to.be(3);
-      expect(rootNode.segments[1].value).to.be('{count,max}-something');
-    });
-
-    it('simple function', function() {
-      var parser = new Parser('sum(test)');
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params.length).to.be(1);
-    });
-
-    it('simple function2', function() {
-      var parser = new Parser('offset(test.metric, -100)');
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params[0].type).to.be('metric');
-      expect(rootNode.params[1].type).to.be('number');
-    });
-
-    it('simple function with string arg', function() {
-      var parser = new Parser("randomWalk('test')");
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params.length).to.be(1);
-      expect(rootNode.params[0].type).to.be('string');
-    });
-
-    it('function with multiple args', function() {
-      var parser = new Parser("sum(test, 1, 'test')");
-      var rootNode = parser.getAst();
-
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params.length).to.be(3);
-      expect(rootNode.params[0].type).to.be('metric');
-      expect(rootNode.params[1].type).to.be('number');
-      expect(rootNode.params[2].type).to.be('string');
-    });
-
-    it('function with nested function', function() {
-      var parser = new Parser("sum(scaleToSeconds(test, 1))");
-      var rootNode = parser.getAst();
-
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params.length).to.be(1);
-      expect(rootNode.params[0].type).to.be('function');
-      expect(rootNode.params[0].name).to.be('scaleToSeconds');
-      expect(rootNode.params[0].params.length).to.be(2);
-      expect(rootNode.params[0].params[0].type).to.be('metric');
-      expect(rootNode.params[0].params[1].type).to.be('number');
-    });
-
-    it('function with multiple series', function() {
-      var parser = new Parser("sum(test.test.*.count, test.timers.*.count)");
-      var rootNode = parser.getAst();
-
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params.length).to.be(2);
-      expect(rootNode.params[0].type).to.be('metric');
-      expect(rootNode.params[1].type).to.be('metric');
-    });
-
-    it('function with templated series', function() {
-      var parser = new Parser("sum(test.[[server]].count)");
-      var rootNode = parser.getAst();
-
-      expect(rootNode.message).to.be(undefined);
-      expect(rootNode.params[0].type).to.be('metric');
-      expect(rootNode.params[0].segments[1].type).to.be('segment');
-      expect(rootNode.params[0].segments[1].value).to.be('[[server]]');
-    });
-
-    it('invalid metric expression', function() {
-      var parser = new Parser('metric.test.*.asd.');
-      var rootNode = parser.getAst();
-
-      expect(rootNode.message).to.be('Expected metric identifier instead found end of string');
-      expect(rootNode.pos).to.be(19);
-    });
-
-    it('invalid function expression missing closing parenthesis', function() {
-      var parser = new Parser('sum(test');
-      var rootNode = parser.getAst();
-
-      expect(rootNode.message).to.be('Expected closing parenthesis instead found end of string');
-      expect(rootNode.pos).to.be(9);
-    });
-
-    it('unclosed string in function', function() {
-      var parser = new Parser("sum('test)");
-      var rootNode = parser.getAst();
-
-      expect(rootNode.message).to.be('Unclosed string parameter');
-      expect(rootNode.pos).to.be(11);
-    });
-
-    it('handle issue #69', function() {
-      var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))');
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('function');
-    });
-
-    it('handle float function arguments', function() {
-      var parser = new Parser('scale(test, 0.002)');
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params[1].type).to.be('number');
-      expect(rootNode.params[1].value).to.be(0.002);
-    });
-
-    it('handle curly brace pattern at start', function() {
-      var parser = new Parser('{apps}.test');
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('metric');
-      expect(rootNode.segments[0].value).to.be('{apps}');
-      expect(rootNode.segments[1].value).to.be('test');
-    });
-
-    it('series parameters', function() {
-      var parser = new Parser('asPercent(#A, #B)');
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params[0].type).to.be('series-ref');
-      expect(rootNode.params[0].value).to.be('#A');
-      expect(rootNode.params[1].value).to.be('#B');
-    });
-
-    it('series parameters, issue 2788', function() {
-      var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)");
-      var rootNode = parser.getAst();
-      expect(rootNode.type).to.be('function');
-      expect(rootNode.params[0].type).to.be('function');
-      expect(rootNode.params[1].value).to.be('10m');
-      expect(rootNode.params[3].type).to.be('bool');
-    });
-
-    it('should parse metric expression with ip number segments', function() {
-      var parser = new Parser('5.10.123.5');
-      var rootNode = parser.getAst();
-      expect(rootNode.segments[0].value).to.be('5');
-      expect(rootNode.segments[1].value).to.be('10');
-      expect(rootNode.segments[2].value).to.be('123');
-      expect(rootNode.segments[3].value).to.be('5');
-    });
-
-  });
-
-});

+ 1 - 1
tslint.json

@@ -28,7 +28,7 @@
     "no-inferrable-types": true,
     "no-shadowed-variable": false,
     "no-string-literal": false,
-    "no-switch-case-fall-through": true,
+    "no-switch-case-fall-through": false,
     "no-trailing-comma": true,
     "no-trailing-whitespace": true,
     "no-unused-expression": false,