Browse Source

feat(plugins): migrated influxdb query editor to new plugin model

Torkel Ödegaard 10 years ago
parent
commit
eecf844ca2

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

@@ -60,7 +60,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope) {
           });
         });
       }
-      // QueryOptionsCtrl
+      // Annotations
       case "annotations-query-ctrl": {
         return System.import(scope.currentDatasource.meta.module).then(function(dsModule) {
           return {

+ 11 - 14
public/app/features/panel/metrics_panel_ctrl.ts

@@ -38,13 +38,6 @@ class MetricsPanelCtrl extends PanelCtrl {
     if (!this.panel.targets) {
       this.panel.targets = [{}];
     }
-
-    // hookup initial data fetch
-    this.$timeout(() => {
-      if (!this.skipDataOnInit) {
-        this.refresh();
-      }
-    }, 30);;
   }
 
   initEditMode() {
@@ -182,15 +175,19 @@ class MetricsPanelCtrl extends PanelCtrl {
     };
 
     this.setTimeQueryStart();
-    return datasource.query(metricsQuery).then(results => {
-      this.setTimeQueryEnd();
+    try {
+      return datasource.query(metricsQuery).then(results => {
+        this.setTimeQueryEnd();
 
-      if (this.dashboard.snapshot) {
-        this.panel.snapshotData = results;
-      }
+        if (this.dashboard.snapshot) {
+          this.panel.snapshotData = results;
+        }
 
-      return results;
-    });
+        return results;
+      });
+    } catch (err) {
+      return this.$q.reject(err);
+    }
   }
 
   setDatasource(datasource) {

+ 1 - 1
public/app/features/panel/partials/panel.html

@@ -1,6 +1,6 @@
 <div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
 	<div class="panel-header">
-		<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="ctrl.error">
+		<span class="alert-error panel-error small pointer" config-modal="public/app/partials/inspector.html" ng-if="ctrl.error">
 			<span data-placement="top" bs-tooltip="ctrl.error">
 				<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
 			</span>

+ 2 - 2
public/app/features/panel/partials/query_editor_row.html

@@ -1,7 +1,7 @@
 <div class="tight-form">
 	<ul class="tight-form-list pull-right">
-		<li ng-show="ctrl.parserError" class="tight-form-item">
-			<a bs-tooltip="ctrl.parserError" style="color: rgb(229, 189, 28)" role="menuitem">
+		<li ng-show="ctrl.error" class="tight-form-item">
+			<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">
 				<i class="fa fa-warning"></i>
 			</a>
 		</li>

+ 1 - 0
public/app/features/panel/query_ctrl.ts

@@ -9,6 +9,7 @@ export class QueryCtrl {
   panelCtrl: any;
   panel: any;
   hasRawMode: boolean;
+  error: string;
 
   constructor(public $scope, private $injector) {
     this.panel = this.panelCtrl.panel;

+ 7 - 8
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -15,7 +15,6 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
   functions: any[];
   segments: any[];
-  parserError: string;
 
   /** @ngInject **/
   constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
@@ -35,7 +34,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   parseTarget() {
     this.functions = [];
     this.segments = [];
-    delete this.parserError;
+    this.error = null;
 
     if (this.target.textEditor) {
       return;
@@ -49,7 +48,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     }
 
     if (astNode.type === 'error') {
-      this.parserError = astNode.message + " at position: " + astNode.pos;
+      this.error = astNode.message + " at position: " + astNode.pos;
       this.target.textEditor = true;
       return;
     }
@@ -58,7 +57,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
       this.parseTargeRecursive(astNode, null, 0);
     } catch (err) {
       console.log('error parsing target:', err.message);
-      this.parserError = err.message;
+      this.error = err.message;
       this.target.textEditor = true;
     }
 
@@ -142,7 +141,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
         }
       }
     }).catch(err => {
-      this.parserError = err.message || 'Failed to issue metric query';
+      this.error = err.message || 'Failed to issue metric query';
     });
   }
 
@@ -179,13 +178,13 @@ export class GraphiteQueryCtrl extends QueryCtrl {
       altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
       return altSegments;
     }).catch(err => {
-      this.parserError = err.message || 'Failed to issue metric query';
+      this.error = err.message || 'Failed to issue metric query';
       return [];
     });
   }
 
   segmentValueChanged(segment, segmentIndex) {
-    delete this.parserError;
+    this.error = null;
 
     if (this.functions.length > 0 && this.functions[0].def.fake) {
       this.functions = [];
@@ -210,7 +209,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   }
 
   targetChanged() {
-    if (this.parserError) {
+    if (this.error) {
       return;
     }
 

+ 0 - 220
public/app/plugins/datasource/influxdb/datasource.js

@@ -1,220 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'app/core/utils/datemath',
-  './influx_series',
-  './influx_query',
-],
-function (angular, _, dateMath, InfluxSeries, InfluxQuery) {
-  'use strict';
-
-  InfluxQuery = InfluxQuery.default;
-
-  /** @ngInject */
-  function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) {
-    this.type = 'influxdb';
-    this.urls = _.map(instanceSettings.url.split(','), function(url) {
-      return url.trim();
-    });
-
-    this.username = instanceSettings.username;
-    this.password = instanceSettings.password;
-    this.name = instanceSettings.name;
-    this.database = instanceSettings.database;
-    this.basicAuth = instanceSettings.basicAuth;
-
-    this.supportAnnotations = true;
-    this.supportMetrics = true;
-
-    this.query = function(options) {
-      var timeFilter = getTimeFilter(options);
-      var queryTargets = [];
-      var i, y;
-
-      var allQueries = _.map(options.targets, function(target) {
-        if (target.hide) { return []; }
-
-        queryTargets.push(target);
-
-        // build query
-        var queryModel = new InfluxQuery(target);
-        var query =  queryModel.render();
-        query = query.replace(/\$interval/g, (target.interval || options.interval));
-        return query;
-
-      }).join("\n");
-
-      // replace grafana variables
-      allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
-
-      // replace templated variables
-      allQueries = templateSrv.replace(allQueries, options.scopedVars);
-
-      return this._seriesQuery(allQueries).then(function(data) {
-        if (!data || !data.results) {
-          return [];
-        }
-
-        var seriesList = [];
-        for (i = 0; i < data.results.length; i++) {
-          var result = data.results[i];
-          if (!result || !result.series) { continue; }
-
-          var target = queryTargets[i];
-          var alias = target.alias;
-          if (alias) {
-            alias = templateSrv.replace(target.alias, options.scopedVars);
-          }
-
-          var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias });
-
-          switch(target.resultFormat) {
-            case 'table': {
-              seriesList.push(influxSeries.getTable());
-              break;
-            }
-            default: {
-              var timeSeries = influxSeries.getTimeSeries();
-              for (y = 0; y < timeSeries.length; y++) {
-                seriesList.push(timeSeries[y]);
-              }
-              break;
-            }
-          }
-        }
-
-        return { data: seriesList };
-      });
-    };
-
-    this.annotationQuery = function(options) {
-      var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw});
-      var query = options.annotation.query.replace('$timeFilter', timeFilter);
-      query = templateSrv.replace(query);
-
-      return this._seriesQuery(query).then(function(data) {
-        if (!data || !data.results || !data.results[0]) {
-          throw { message: 'No results in response from InfluxDB' };
-        }
-        return new InfluxSeries({series: data.results[0].series, annotation: options.annotation}).getAnnotations();
-      });
-    };
-
-    this.metricFindQuery = function (query) {
-      var interpolated;
-      try {
-        interpolated = templateSrv.replace(query);
-      }
-      catch (err) {
-        return $q.reject(err);
-      }
-
-      return this._seriesQuery(interpolated).then(function (results) {
-        if (!results || results.results.length === 0) { return []; }
-
-        var influxResults = results.results[0];
-        if (!influxResults.series) {
-          return [];
-        }
-
-        var series = influxResults.series[0];
-        return _.map(series.values, function(value) {
-          if (_.isArray(value)) {
-            return { text: value[0] };
-          } else {
-            return { text: value };
-          }
-        });
-      });
-    };
-
-    this._seriesQuery = function(query) {
-      return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
-    };
-
-    this.testDatasource = function() {
-      return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
-        return { status: "success", message: "Data source is working", title: "Success" };
-      });
-    };
-
-    this._influxRequest = function(method, url, data) {
-      var self = this;
-
-      var currentUrl = self.urls.shift();
-      self.urls.push(currentUrl);
-
-      var params = {
-        u: self.username,
-        p: self.password,
-      };
-
-      if (self.database) {
-        params.db = self.database;
-      }
-
-      if (method === 'GET') {
-        _.extend(params, data);
-        data = null;
-      }
-
-      var options = {
-        method: method,
-        url:    currentUrl + url,
-        params: params,
-        data:   data,
-        precision: "ms",
-        inspect: { type: 'influxdb' },
-      };
-
-      options.headers = options.headers || {};
-      if (self.basicAuth) {
-        options.headers.Authorization = self.basicAuth;
-      }
-
-      return backendSrv.datasourceRequest(options).then(function(result) {
-        return result.data;
-      }, function(err) {
-        if (err.status !== 0 || err.status >= 300) {
-          if (err.data && err.data.error) {
-            throw { message: 'InfluxDB Error Response: ' + err.data.error, data: err.data, config: err.config };
-          }
-          else {
-            throw { message: 'InfluxDB Error: ' + err.message, data: err.data, config: err.config };
-          }
-        }
-      });
-    };
-
-    function getTimeFilter(options) {
-      var from = getInfluxTime(options.rangeRaw.from, false);
-      var until = getInfluxTime(options.rangeRaw.to, true);
-      var fromIsAbsolute = from[from.length-1] === 's';
-
-      if (until === 'now()' && !fromIsAbsolute) {
-        return 'time > ' + from;
-      }
-
-      return 'time > ' + from + ' and time < ' + until;
-    }
-
-    function getInfluxTime(date, roundUp) {
-      if (_.isString(date)) {
-        if (date === 'now') {
-          return 'now()';
-        }
-
-        var parts = /^now-(\d+)([d|h|m|s])$/.exec(date);
-        if (parts) {
-          var amount = parseInt(parts[1]);
-          var unit = parts[2];
-          return 'now() - ' + amount + unit;
-        }
-        date = dateMath.parse(date, roundUp);
-      }
-      return (date.valueOf() / 1000).toFixed(0) + 's';
-    }
-  }
-
-  return InfluxDatasource;
-});

+ 213 - 0
public/app/plugins/datasource/influxdb/datasource.ts

@@ -0,0 +1,213 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+
+import * as dateMath from 'app/core/utils/datemath';
+import InfluxSeries from './influx_series';
+import InfluxQuery from './influx_query';
+
+/** @ngInject */
+export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+  this.type = 'influxdb';
+  this.urls = _.map(instanceSettings.url.split(','), function(url) {
+    return url.trim();
+  });
+
+  this.username = instanceSettings.username;
+  this.password = instanceSettings.password;
+  this.name = instanceSettings.name;
+  this.database = instanceSettings.database;
+  this.basicAuth = instanceSettings.basicAuth;
+
+  this.supportAnnotations = true;
+  this.supportMetrics = true;
+
+  this.query = function(options) {
+    var timeFilter = getTimeFilter(options);
+    var queryTargets = [];
+    var i, y;
+
+    var allQueries = _.map(options.targets, function(target) {
+      if (target.hide) { return []; }
+
+      queryTargets.push(target);
+
+      // build query
+      var queryModel = new InfluxQuery(target);
+      var query =  queryModel.render();
+      query = query.replace(/\$interval/g, (target.interval || options.interval));
+      return query;
+
+    }).join("\n");
+
+    // replace grafana variables
+    allQueries = allQueries.replace(/\$timeFilter/g, timeFilter);
+
+    // replace templated variables
+    allQueries = templateSrv.replace(allQueries, options.scopedVars);
+
+    return this._seriesQuery(allQueries).then(function(data): any {
+      if (!data || !data.results) {
+        return [];
+      }
+
+      var seriesList = [];
+      for (i = 0; i < data.results.length; i++) {
+        var result = data.results[i];
+        if (!result || !result.series) { continue; }
+
+        var target = queryTargets[i];
+        var alias = target.alias;
+        if (alias) {
+          alias = templateSrv.replace(target.alias, options.scopedVars);
+        }
+
+        var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias });
+
+        switch (target.resultFormat) {
+          case 'table': {
+            seriesList.push(influxSeries.getTable());
+            break;
+          }
+          default: {
+            var timeSeries = influxSeries.getTimeSeries();
+            for (y = 0; y < timeSeries.length; y++) {
+              seriesList.push(timeSeries[y]);
+            }
+            break;
+          }
+        }
+      }
+
+      return { data: seriesList };
+    });
+  };
+
+  this.annotationQuery = function(options) {
+    var timeFilter = getTimeFilter({rangeRaw: options.rangeRaw});
+    var query = options.annotation.query.replace('$timeFilter', timeFilter);
+    query = templateSrv.replace(query);
+
+    return this._seriesQuery(query).then(function(data) {
+      if (!data || !data.results || !data.results[0]) {
+        throw { message: 'No results in response from InfluxDB' };
+      }
+      return new InfluxSeries({series: data.results[0].series, annotation: options.annotation}).getAnnotations();
+    });
+  };
+
+  this.metricFindQuery = function (query) {
+    var interpolated;
+    try {
+      interpolated = templateSrv.replace(query);
+    } catch (err) {
+      return $q.reject(err);
+    }
+
+    return this._seriesQuery(interpolated).then(function (results) {
+      if (!results || results.results.length === 0) { return []; }
+
+      var influxResults = results.results[0];
+      if (!influxResults.series) {
+        return [];
+      }
+
+      var series = influxResults.series[0];
+      return _.map(series.values, function(value) {
+        if (_.isArray(value)) {
+          return { text: value[0] };
+        } else {
+          return { text: value };
+        }
+      });
+    });
+  };
+
+  this._seriesQuery = function(query) {
+    return this._influxRequest('GET', '/query', {q: query, epoch: 'ms'});
+  };
+
+  this.testDatasource = function() {
+    return this.metricFindQuery('SHOW MEASUREMENTS LIMIT 1').then(function () {
+      return { status: "success", message: "Data source is working", title: "Success" };
+    });
+  };
+
+  this._influxRequest = function(method, url, data) {
+    var self = this;
+
+    var currentUrl = self.urls.shift();
+    self.urls.push(currentUrl);
+
+    var params: any = {
+      u: self.username,
+      p: self.password,
+    };
+
+    if (self.database) {
+      params.db = self.database;
+    }
+
+    if (method === 'GET') {
+      _.extend(params, data);
+      data = null;
+    }
+
+    var options: any = {
+      method: method,
+      url:    currentUrl + url,
+      params: params,
+      data:   data,
+      precision: "ms",
+      inspect: { type: 'influxdb' },
+    };
+
+    options.headers = options.headers || {};
+    if (self.basicAuth) {
+      options.headers.Authorization = self.basicAuth;
+    }
+
+    return backendSrv.datasourceRequest(options).then(function(result) {
+      return result.data;
+    }, function(err) {
+      if (err.status !== 0 || err.status >= 300) {
+        if (err.data && err.data.error) {
+          throw { message: 'InfluxDB Error Response: ' + err.data.error, data: err.data, config: err.config };
+        } else {
+          throw { message: 'InfluxDB Error: ' + err.message, data: err.data, config: err.config };
+        }
+      }
+    });
+  };
+
+  function getTimeFilter(options) {
+    var from = getInfluxTime(options.rangeRaw.from, false);
+    var until = getInfluxTime(options.rangeRaw.to, true);
+    var fromIsAbsolute = from[from.length-1] === 's';
+
+    if (until === 'now()' && !fromIsAbsolute) {
+      return 'time > ' + from;
+    }
+
+    return 'time > ' + from + ' and time < ' + until;
+  }
+
+  function getInfluxTime(date, roundUp) {
+    if (_.isString(date)) {
+      if (date === 'now') {
+        return 'now()';
+      }
+
+      var parts = /^now-(\d+)([d|h|m|s])$/.exec(date);
+      if (parts) {
+        var amount = parseInt(parts[1]);
+        var unit = parts[2];
+        return 'now() - ' + amount + unit;
+      }
+      date = dateMath.parse(date, roundUp);
+    }
+    return (date.valueOf() / 1000).toFixed(0) + 's';
+  }
+}
+

+ 0 - 30
public/app/plugins/datasource/influxdb/module.js

@@ -1,30 +0,0 @@
-define([
-  './datasource',
-],
-function (InfluxDatasource) {
-  'use strict';
-
-  function influxMetricsQueryEditor() {
-    return {controller: 'InfluxQueryCtrl', templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.editor.html'};
-  }
-
-  function influxMetricsQueryOptions() {
-    return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.options.html'};
-  }
-
-  function influxAnnotationsQueryEditor() {
-    return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/annotations.editor.html'};
-  }
-
-  function influxConfigView() {
-    return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/config.html'};
-  }
-
-  return {
-    Datasource:               InfluxDatasource,
-    metricsQueryEditor:       influxMetricsQueryEditor,
-    metricsQueryOptions:      influxMetricsQueryOptions,
-    annotationsQueryEditor:   influxAnnotationsQueryEditor,
-    configView:               influxConfigView,
-  };
-});

+ 53 - 0
public/app/plugins/datasource/influxdb/module.ts

@@ -0,0 +1,53 @@
+import {InfluxDatasource} from './datasource';
+import {InfluxQueryCtrl} from './query_ctrl';
+
+class InfluxConfigCtrl {
+  static templateUrl = 'public/app/plugins/datasource/influxdb/partials/config.html';
+}
+
+class InfluxQueryOptionsCtrl {
+  static templateUrl = 'public/app/plugins/datasource/influxdb/partials/query.options.html';
+}
+
+class InfluxAnnotationsQueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/influxdb/partials/annotations.editor.html';
+}
+
+export {
+  InfluxDatasource as Datasource,
+  InfluxQueryCtrl as QueryCtrl,
+  InfluxConfigCtrl as ConfigCtrl,
+  InfluxQueryOptionsCtrl as QueryOptionsCtrl,
+  InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl,
+};
+
+// define([
+//   './datasource',
+// ],
+// function (InfluxDatasource) {
+//   'use strict';
+//
+//   function influxMetricsQueryEditor() {
+//     return {controller: 'InfluxQueryCtrl', templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.editor.html'};
+//   }
+//
+//   function influxMetricsQueryOptions() {
+//     return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/query.options.html'};
+//   }
+//
+//   function influxAnnotationsQueryEditor() {
+//     return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/annotations.editor.html'};
+//   }
+//
+//   function influxConfigView() {
+//     return {templateUrl: 'public/app/plugins/datasource/influxdb/partials/config.html'};
+//   }
+//
+//   return {
+//     Datasource:               InfluxDatasource,
+//     metricsQueryEditor:       influxMetricsQueryEditor,
+//     metricsQueryOptions:      influxMetricsQueryOptions,
+//     annotationsQueryEditor:   influxAnnotationsQueryEditor,
+//     configView:               influxConfigView,
+//   };
+// });

+ 42 - 88
public/app/plugins/datasource/influxdb/partials/query.editor.html

@@ -1,119 +1,73 @@
-<div class="">
-	<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">
-					<i class="fa fa-warning"></i>
-				</a>
-			</li>
-			<li class="tight-form-item small" ng-show="target.datasource">
-				<em>{{target.datasource}}</em>
-			</li>
-			<li class="tight-form-item">
-				<div class="dropdown">
-					<a  class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
-						<i class="fa fa-bars"></i>
-					</a>
-					<ul class="dropdown-menu pull-right" role="menu">
-						<li role="menuitem"><a tabindex="1" ng-click="toggleQueryMode()">Switch editor mode</a></li>
-						<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.duplicateDataQuery(target)">Duplicate</a></li>
-						<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index-1)">Move up</a></li>
-						<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index+1)">Move down</a></li>
-					</ul>
-				</div>
-			</li>
-
-			<li class="tight-form-item last">
-				<a class="pointer" tabindex="1" ng-click="panelCtrl.removeDataQuery(target)">
-					<i class="fa fa-remove"></i>
-				</a>
-			</li>
-		</ul>
-
-		<ul class="tight-form-list">
-			<li class="tight-form-item" style="min-width: 15px; text-align: center">
-				{{target.refId}}
-			</li>
-			<li>
-				<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
-					<i class="fa fa-eye"></i>
-				</a>
-			</li>
-		</ul>
-
-		<ul class="tight-form-list" ng-hide="target.rawQuery">
+<query-editor-row ctrl="ctrl">
+		<ul class="tight-form-list" ng-hide="ctrl.target.rawQuery">
 			<li class="tight-form-item query-keyword" style="width: 75px">
 				FROM
 			</li>
 			<li>
-				<metric-segment segment="policySegment" get-options="getPolicySegments()" on-change="policyChanged()"></metric-segment>
+				<metric-segment segment="ctrl.policySegment" get-options="ctrl.getPolicySegments()" on-change="ctrl.policyChanged()"></metric-segment>
 			</li>
 			<li>
-				<metric-segment segment="measurementSegment" get-options="getMeasurements()" on-change="measurementChanged()"></metric-segment>
+				<metric-segment segment="ctrl.measurementSegment" get-options="ctrl.getMeasurements()" on-change="ctrl.measurementChanged()"></metric-segment>
 			</li>
 			<li class="tight-form-item query-keyword" style="padding-left: 15px; padding-right: 15px;">
 				WHERE
 			</li>
-			<li ng-repeat="segment in tagSegments">
-				<metric-segment segment="segment" get-options="getTagsOrValues(segment, $index)" on-change="tagSegmentUpdated(segment, $index)"></metric-segment>
+			<li ng-repeat="segment in ctrl.tagSegments">
+				<metric-segment segment="segment" get-options="ctrl.getTagsOrValues(segment, $index)" on-change="ctrl.tagSegmentUpdated(segment, $index)"></metric-segment>
 			</li>
 		</ul>
 
 		<div class="tight-form-flex-wrapper" ng-show="target.rawQuery">
-			<input type="text" class="tight-form-clear-input" ng-model="target.query" spellcheck="false" style="width: 100%;" ng-blur="panelCtrl.refresh()"></input>
+			<input type="text" class="tight-form-clear-input" ng-model="ctrl.target.query" spellcheck="false" style="width: 100%;" ng-blur="ctrl.refresh()"></input>
 		</div>
+</query-editor-row>
 
+<div ng-hide="ctrl.target.rawQuery">
+	<div class="tight-form" ng-repeat="selectParts in ctrl.queryModel.selectModels">
+		<ul class="tight-form-list">
+			<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
+				<span ng-show="$index === 0">SELECT</span>
+			</li>
+			<li ng-repeat="part in selectParts">
+				<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="ctrl.removeSelectPart(selectParts, part)" part-updated="ctrl.selectPartUpdated(selectParts, part)" get-options="ctrl.getPartOptions(part)"></influx-query-part-editor>
+			</li>
+			<li class="dropdown" dropdown-typeahead="ctrl.selectMenu" dropdown-typeahead-on-select="ctrl.addSelectPart(selectParts, $item, $subItem)">
+			</li>
+		</ul>
 		<div class="clearfix"></div>
 	</div>
 
-	<div ng-hide="target.rawQuery">
-
-		<div class="tight-form" ng-repeat="selectParts in queryModel.selectModels">
-			<ul class="tight-form-list">
-				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-					<span ng-show="$index === 0">SELECT</span>
-				</li>
-				<li ng-repeat="part in selectParts">
-					<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeSelectPart(selectParts, part)" part-updated="selectPartUpdated(selectParts, part)" get-options="getPartOptions(part)"></influx-query-part-editor>
-				</li>
-				<li class="dropdown" dropdown-typeahead="selectMenu" dropdown-typeahead-on-select="addSelectPart(selectParts, $item, $subItem)">
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
-
-		<div class="tight-form">
-			<ul class="tight-form-list">
-				<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-					<span>GROUP BY</span>
-				</li>
-				<li ng-repeat="part in queryModel.groupByParts">
-					<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="removeGroupByPart(part, $index)" part-updated="panelCtrl.refresh();" get-options="getPartOptions(part)"></influx-query-part-editor>
-				</li>
-				<li>
-					<metric-segment segment="groupBySegment" get-options="getGroupByOptions()" on-change="groupByAction(part, $index)"></metric-segment>
-				</li>
-			</ul>
-			<div class="clearfix"></div>
-		</div>
-	</div>
-
 	<div class="tight-form">
 		<ul class="tight-form-list">
 			<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
-				ALIAS BY
+				<span>GROUP BY</span>
 			</li>
-			<li>
-				<input type="text" class="tight-form-clear-input input-xlarge" ng-model="target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="panelCtrl.refresh()">
-			</li>
-			<li class="tight-form-item">
-				Format as
+			<li ng-repeat="part in ctrl.queryModel.groupByParts">
+				<influx-query-part-editor part="part" class="tight-form-item tight-form-func" remove-action="ctrl.removeGroupByPart(part, $index)" part-updated="ctrl.refresh();" get-options="ctrl.getPartOptions(part)"></influx-query-part-editor>
 			</li>
 			<li>
-				<select class="input-small tight-form-input" style="width: 104px" ng-model="target.resultFormat" ng-options="f.value as f.text for f in resultFormats" ng-change="panelCtrl.refresh()"></select>
+				<metric-segment segment="ctrl.groupBySegment" get-options="ctrl.getGroupByOptions()" on-change="ctrl.groupByAction(part, $index)"></metric-segment>
 			</li>
 		</ul>
 		<div class="clearfix"></div>
 	</div>
+</div>
 
+<div class="tight-form">
+	<ul class="tight-form-list">
+		<li class="tight-form-item query-keyword tight-form-align" style="width: 75px;">
+			ALIAS BY
+		</li>
+		<li>
+			<input type="text" class="tight-form-clear-input input-xlarge" ng-model="ctrl.target.alias" spellcheck='false' placeholder="Naming pattern" ng-blur="ctrl.refresh()">
+		</li>
+		<li class="tight-form-item">
+			Format as
+		</li>
+		<li>
+			<select class="input-small tight-form-input" style="width: 104px" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
+		</li>
+	</ul>
+	<div class="clearfix"></div>
 </div>
+

+ 0 - 322
public/app/plugins/datasource/influxdb/query_ctrl.js

@@ -1,322 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  './query_builder',
-  './influx_query',
-  './query_part',
-  './query_part_editor',
-],
-function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) {
-  'use strict';
-
-  var module = angular.module('grafana.controllers');
-
-  InfluxQuery = InfluxQuery.default;
-  queryPart = queryPart.default;
-
-  module.controller('InfluxQueryCtrl', function($scope, templateSrv, $q, uiSegmentSrv) {
-    var panelCtrl = $scope.ctrl;
-    var datasource = $scope.datasource;
-    $scope.panelCtrl = panelCtrl;
-
-    $scope.init = function() {
-      if (!$scope.target) { return; }
-
-      $scope.target = $scope.target;
-      $scope.queryModel = new InfluxQuery($scope.target);
-      $scope.queryBuilder = new InfluxQueryBuilder($scope.target, datasource.database);
-      $scope.groupBySegment = uiSegmentSrv.newPlusButton();
-      $scope.resultFormats = [
-         {text: 'Time series', value: 'time_series'},
-         {text: 'Table', value: 'table'},
-      ];
-
-      $scope.policySegment = uiSegmentSrv.newSegment($scope.target.policy);
-
-      if (!$scope.target.measurement) {
-        $scope.measurementSegment = uiSegmentSrv.newSelectMeasurement();
-      } else {
-        $scope.measurementSegment = uiSegmentSrv.newSegment($scope.target.measurement);
-      }
-
-      $scope.tagSegments = [];
-      _.each($scope.target.tags, function(tag) {
-        if (!tag.operator) {
-          if (/^\/.*\/$/.test(tag.value)) {
-            tag.operator = "=~";
-          } else {
-            tag.operator = '=';
-          }
-        }
-
-        if (tag.condition) {
-          $scope.tagSegments.push(uiSegmentSrv.newCondition(tag.condition));
-        }
-
-        $scope.tagSegments.push(uiSegmentSrv.newKey(tag.key));
-        $scope.tagSegments.push(uiSegmentSrv.newOperator(tag.operator));
-        $scope.tagSegments.push(uiSegmentSrv.newKeyValue(tag.value));
-      });
-
-      $scope.fixTagSegments();
-      $scope.buildSelectMenu();
-      $scope.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
-    };
-
-    $scope.buildSelectMenu = function() {
-      var categories = queryPart.getCategories();
-      $scope.selectMenu = _.reduce(categories, function(memo, cat, key) {
-        var menu = {text: key};
-        menu.submenu = _.map(cat, function(item) {
-          return {text: item.type, value: item.type};
-        });
-        memo.push(menu);
-        return memo;
-      }, []);
-    };
-
-    $scope.getGroupByOptions = function() {
-      var query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
-
-      return datasource.metricFindQuery(query)
-      .then(function(tags) {
-        var options = [];
-        if (!$scope.queryModel.hasFill()) {
-          options.push(uiSegmentSrv.newSegment({value: 'fill(null)'}));
-        }
-        if (!$scope.queryModel.hasGroupByTime()) {
-          options.push(uiSegmentSrv.newSegment({value: 'time($interval)'}));
-        }
-        _.each(tags, function(tag) {
-          options.push(uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'}));
-        });
-        return options;
-      })
-      .then(null, $scope.handleQueryError);
-    };
-
-    $scope.groupByAction = function() {
-      $scope.queryModel.addGroupBy($scope.groupBySegment.value);
-      var plusButton = uiSegmentSrv.newPlusButton();
-      $scope.groupBySegment.value  = plusButton.value;
-      $scope.groupBySegment.html  = plusButton.html;
-      panelCtrl.refresh();
-    };
-
-    $scope.removeGroupByPart = function(part, index) {
-      $scope.queryModel.removeGroupByPart(part, index);
-      panelCtrl.refresh();
-    };
-
-    $scope.addSelectPart = function(selectParts, cat, subitem) {
-      $scope.queryModel.addSelectPart(selectParts, subitem.value);
-      panelCtrl.refresh();
-    };
-
-    $scope.removeSelectPart = function(selectParts, part) {
-      $scope.queryModel.removeSelectPart(selectParts, part);
-      panelCtrl.refresh();
-    };
-
-    $scope.selectPartUpdated = function() {
-      panelCtrl.refresh();
-    };
-
-    $scope.fixTagSegments = function() {
-      var count = $scope.tagSegments.length;
-      var lastSegment = $scope.tagSegments[Math.max(count-1, 0)];
-
-      if (!lastSegment || lastSegment.type !== 'plus-button') {
-        $scope.tagSegments.push(uiSegmentSrv.newPlusButton());
-      }
-    };
-
-    $scope.measurementChanged = function() {
-      $scope.target.measurement = $scope.measurementSegment.value;
-      panelCtrl.refresh();
-    };
-
-    $scope.getPolicySegments = function() {
-      var policiesQuery = $scope.queryBuilder.buildExploreQuery('RETENTION POLICIES');
-      return datasource.metricFindQuery(policiesQuery)
-      .then($scope.transformToSegments(false))
-      .then(null, $scope.handleQueryError);
-    };
-
-    $scope.policyChanged = function() {
-      $scope.target.policy = $scope.policySegment.value;
-      panelCtrl.refresh();
-    };
-
-    $scope.toggleQueryMode = function () {
-      $scope.target.rawQuery = !$scope.target.rawQuery;
-    };
-
-    $scope.getMeasurements = function () {
-      var query = $scope.queryBuilder.buildExploreQuery('MEASUREMENTS');
-      return datasource.metricFindQuery(query)
-      .then($scope.transformToSegments(true), $scope.handleQueryError);
-    };
-
-    $scope.getPartOptions = function(part) {
-      if (part.def.type === 'field') {
-        var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
-        return datasource.metricFindQuery(fieldsQuery)
-        .then($scope.transformToSegments(true), $scope.handleQueryError);
-      }
-      if (part.def.type === 'tag') {
-        var tagsQuery = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
-        return datasource.metricFindQuery(tagsQuery)
-        .then($scope.transformToSegments(true), $scope.handleQueryError);
-      }
-    };
-
-    $scope.handleQueryError = function(err) {
-      $scope.parserError = err.message || 'Failed to issue metric query';
-      return [];
-    };
-
-    $scope.transformToSegments = function(addTemplateVars) {
-      return function(results) {
-        var segments = _.map(results, function(segment) {
-          return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
-        });
-
-        if (addTemplateVars) {
-          _.each(templateSrv.variables, function(variable) {
-            segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true }));
-          });
-        }
-
-        return segments;
-      };
-    };
-
-    $scope.getTagsOrValues = function(segment, index) {
-      if (segment.type === 'condition') {
-        return $q.when([uiSegmentSrv.newSegment('AND'), uiSegmentSrv.newSegment('OR')]);
-      }
-      if (segment.type === 'operator') {
-        var nextValue = $scope.tagSegments[index+1].value;
-        if (/^\/.*\/$/.test(nextValue)) {
-          return $q.when(uiSegmentSrv.newOperators(['=~', '!~']));
-        } else {
-          return $q.when(uiSegmentSrv.newOperators(['=', '<>', '<', '>']));
-        }
-      }
-
-      var query, addTemplateVars;
-      if (segment.type === 'key' || segment.type === 'plus-button') {
-        query = $scope.queryBuilder.buildExploreQuery('TAG_KEYS');
-        addTemplateVars = false;
-      } else if (segment.type === 'value')  {
-        query = $scope.queryBuilder.buildExploreQuery('TAG_VALUES', $scope.tagSegments[index-2].value);
-        addTemplateVars = true;
-      }
-
-      return datasource.metricFindQuery(query)
-      .then($scope.transformToSegments(addTemplateVars))
-      .then(function(results) {
-        if (segment.type === 'key') {
-          results.splice(0, 0, angular.copy($scope.removeTagFilterSegment));
-        }
-        return results;
-      })
-      .then(null, $scope.handleQueryError);
-    };
-
-    $scope.getFieldSegments = function() {
-      var fieldsQuery = $scope.queryBuilder.buildExploreQuery('FIELDS');
-      return datasource.metricFindQuery(fieldsQuery)
-      .then($scope.transformToSegments(false))
-      .then(null, $scope.handleQueryError);
-    };
-
-    $scope.getTagOptions = function() {
-   };
-
-    $scope.setFill = function(fill) {
-      $scope.target.fill = fill;
-      panelCtrl.refresh();
-    };
-
-    $scope.tagSegmentUpdated = function(segment, index) {
-      $scope.tagSegments[index] = segment;
-
-      // handle remove tag condition
-      if (segment.value === $scope.removeTagFilterSegment.value) {
-        $scope.tagSegments.splice(index, 3);
-        if ($scope.tagSegments.length === 0) {
-          $scope.tagSegments.push(uiSegmentSrv.newPlusButton());
-        } else if ($scope.tagSegments.length > 2) {
-          $scope.tagSegments.splice(Math.max(index-1, 0), 1);
-          if ($scope.tagSegments[$scope.tagSegments.length-1].type !== 'plus-button') {
-            $scope.tagSegments.push(uiSegmentSrv.newPlusButton());
-          }
-        }
-      }
-      else {
-        if (segment.type === 'plus-button') {
-          if (index > 2) {
-            $scope.tagSegments.splice(index, 0, uiSegmentSrv.newCondition('AND'));
-          }
-          $scope.tagSegments.push(uiSegmentSrv.newOperator('='));
-          $scope.tagSegments.push(uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
-          segment.type = 'key';
-          segment.cssClass = 'query-segment-key';
-        }
-
-        if ((index+1) === $scope.tagSegments.length) {
-          $scope.tagSegments.push(uiSegmentSrv.newPlusButton());
-        }
-      }
-
-      $scope.rebuildTargetTagConditions();
-    };
-
-    $scope.rebuildTargetTagConditions = function() {
-      var tags = [];
-      var tagIndex = 0;
-      var tagOperator = "";
-      _.each($scope.tagSegments, function(segment2, index) {
-        if (segment2.type === 'key') {
-          if (tags.length === 0) {
-            tags.push({});
-          }
-          tags[tagIndex].key = segment2.value;
-        }
-        else if (segment2.type === 'value') {
-          tagOperator = $scope.getTagValueOperator(segment2.value, tags[tagIndex].operator);
-          if (tagOperator) {
-            $scope.tagSegments[index-1] = uiSegmentSrv.newOperator(tagOperator);
-            tags[tagIndex].operator = tagOperator;
-          }
-          tags[tagIndex].value = segment2.value;
-        }
-        else if (segment2.type === 'condition') {
-          tags.push({ condition: segment2.value });
-          tagIndex += 1;
-        }
-        else if (segment2.type === 'operator') {
-          tags[tagIndex].operator = segment2.value;
-        }
-      });
-
-      $scope.target.tags = tags;
-      panelCtrl.refresh();
-    };
-
-    $scope.getTagValueOperator = function(tagValue, tagOperator) {
-      if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) {
-        return '=~';
-      }
-      else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) {
-        return '=';
-      }
-    };
-
-    $scope.init();
-
-  });
-
-});

+ 318 - 0
public/app/plugins/datasource/influxdb/query_ctrl.ts

@@ -0,0 +1,318 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import './query_part_editor';
+import './query_part_editor';
+
+import angular from 'angular';
+import _ from 'lodash';
+import InfluxQueryBuilder from './query_builder';
+import InfluxQuery from './influx_query';
+import queryPart from './query_part';
+import {QueryCtrl} from 'app/features/panel/panel';
+
+export class InfluxQueryCtrl extends QueryCtrl {
+  static templateUrl = 'public/app/plugins/datasource/influxdb/partials/query.editor.html';
+
+  queryModel: InfluxQuery;
+  queryBuilder: any;
+  groupBySegment: any;
+  resultFormats: any[];
+  policySegment: any;
+  tagSegments: any[];
+  selectMenu: any;
+  measurementSegment: any;
+  removeTagFilterSegment: any;
+
+  constructor($scope, $injector, private templateSrv, private $q, private uiSegmentSrv) {
+    super($scope, $injector);
+
+    this.target = this.target;
+    this.queryModel = new InfluxQuery(this.target);
+    this.queryBuilder = new InfluxQueryBuilder(this.target, this.datasource.database);
+    this.groupBySegment = this.uiSegmentSrv.newPlusButton();
+    this.resultFormats = [
+      {text: 'Time series', value: 'time_series'},
+      {text: 'Table', value: 'table'},
+    ];
+
+    this.policySegment = uiSegmentSrv.newSegment(this.target.policy);
+
+    if (!this.target.measurement) {
+      this.measurementSegment = uiSegmentSrv.newSelectMeasurement();
+    } else {
+      this.measurementSegment = uiSegmentSrv.newSegment(this.target.measurement);
+    }
+
+    this.tagSegments = [];
+    for (let tag of this.target.tags) {
+      if (!tag.operator) {
+        if (/^\/.*\/$/.test(tag.value)) {
+          tag.operator = "=~";
+        } else {
+          tag.operator = '=';
+        }
+      }
+
+      if (tag.condition) {
+        this.tagSegments.push(uiSegmentSrv.newCondition(tag.condition));
+      }
+
+      this.tagSegments.push(uiSegmentSrv.newKey(tag.key));
+      this.tagSegments.push(uiSegmentSrv.newOperator(tag.operator));
+      this.tagSegments.push(uiSegmentSrv.newKeyValue(tag.value));
+    }
+
+    this.fixTagSegments();
+    this.buildSelectMenu();
+    this.removeTagFilterSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove tag filter --'});
+  }
+
+  buildSelectMenu() {
+    var categories = queryPart.getCategories();
+    this.selectMenu = _.reduce(categories, function(memo, cat, key) {
+      var menu = {
+        text: key,
+        submenu: cat.map(item => {
+         return {text: item.type, value: item.type};
+        }),
+      };
+      memo.push(menu);
+      return memo;
+    }, []);
+  }
+
+  getGroupByOptions() {
+    var query = this.queryBuilder.buildExploreQuery('TAG_KEYS');
+
+    return this.datasource.metricFindQuery(query).then(tags => {
+      var options = [];
+      if (!this.queryModel.hasFill()) {
+        options.push(this.uiSegmentSrv.newSegment({value: 'fill(null)'}));
+      }
+      if (!this.queryModel.hasGroupByTime()) {
+        options.push(this.uiSegmentSrv.newSegment({value: 'time($interval)'}));
+      }
+      for (let tag of tags) {
+        options.push(this.uiSegmentSrv.newSegment({value: 'tag(' + tag.text + ')'}));
+      }
+      return options;
+    }).catch(this.handleQueryError.bind(this));
+  }
+
+  groupByAction() {
+    this.queryModel.addGroupBy(this.groupBySegment.value);
+    var plusButton = this.uiSegmentSrv.newPlusButton();
+    this.groupBySegment.value  = plusButton.value;
+    this.groupBySegment.html  = plusButton.html;
+    this.panelCtrl.refresh();
+  }
+
+  removeGroupByPart(part, index) {
+    this.queryModel.removeGroupByPart(part, index);
+    this.panelCtrl.refresh();
+  }
+
+  addSelectPart(selectParts, cat, subitem) {
+    this.queryModel.addSelectPart(selectParts, subitem.value);
+    this.panelCtrl.refresh();
+  }
+
+  removeSelectPart(selectParts, part) {
+    this.queryModel.removeSelectPart(selectParts, part);
+    this.panelCtrl.refresh();
+  }
+
+  selectPartUpdated() {
+    this.panelCtrl.refresh();
+  }
+
+  fixTagSegments() {
+    var count = this.tagSegments.length;
+    var lastSegment = this.tagSegments[Math.max(count-1, 0)];
+
+    if (!lastSegment || lastSegment.type !== 'plus-button') {
+      this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
+    }
+  }
+
+  measurementChanged() {
+    this.target.measurement = this.measurementSegment.value;
+    this.panelCtrl.refresh();
+  }
+
+  getPolicySegments() {
+    var policiesQuery = this.queryBuilder.buildExploreQuery('RETENTION POLICIES');
+    return this.datasource.metricFindQuery(policiesQuery)
+    .then(this.transformToSegments(false))
+    .catch(this.handleQueryError.bind(this));
+  }
+
+  policyChanged() {
+    this.target.policy = this.policySegment.value;
+    this.panelCtrl.refresh();
+  }
+
+  toggleQueryMode() {
+    this.target.rawQuery = !this.target.rawQuery;
+  }
+
+  getMeasurements() {
+    var query = this.queryBuilder.buildExploreQuery('MEASUREMENTS');
+    return this.datasource.metricFindQuery(query)
+      .then(this.transformToSegments(true))
+      .catch(this.handleQueryError.bind(this));
+  }
+
+  getPartOptions(part) {
+    if (part.def.type === 'field') {
+      var fieldsQuery = this.queryBuilder.buildExploreQuery('FIELDS');
+      return this.datasource.metricFindQuery(fieldsQuery)
+      .then(this.transformToSegments(true))
+      .catch(this.handleQueryError.bind(this));
+    }
+    if (part.def.type === 'tag') {
+      var tagsQuery = this.queryBuilder.buildExploreQuery('TAG_KEYS');
+      return this.datasource.metricFindQuery(tagsQuery)
+      .then(this.transformToSegments(true))
+      .catch(this.handleQueryError.bind(true));
+    }
+  }
+
+  handleQueryError(err) {
+    this.error = err.message || 'Failed to issue metric query';
+    return [];
+  }
+
+  transformToSegments(addTemplateVars) {
+    return (results) => {
+      var segments = _.map(results, segment => {
+        return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
+      });
+
+      if (addTemplateVars) {
+        for (let variable of this.templateSrv.variables) {
+          segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/$' + variable.name + '$/', expandable: true }));
+        }
+      }
+
+      return segments;
+    };
+  }
+
+  getTagsOrValues(segment, index) {
+    if (segment.type === 'condition') {
+      return this.$q.when([this.uiSegmentSrv.newSegment('AND'), this.uiSegmentSrv.newSegment('OR')]);
+    }
+    if (segment.type === 'operator') {
+      var nextValue = this.tagSegments[index+1].value;
+      if (/^\/.*\/$/.test(nextValue)) {
+        return this.$q.when(this.uiSegmentSrv.newOperators(['=~', '!~']));
+      } else {
+        return this.$q.when(this.uiSegmentSrv.newOperators(['=', '<>', '<', '>']));
+      }
+    }
+
+    var query, addTemplateVars;
+    if (segment.type === 'key' || segment.type === 'plus-button') {
+      query = this.queryBuilder.buildExploreQuery('TAG_KEYS');
+      addTemplateVars = false;
+    } else if (segment.type === 'value')  {
+      query = this.queryBuilder.buildExploreQuery('TAG_VALUES', this.tagSegments[index-2].value);
+      addTemplateVars = true;
+    }
+
+    return this.datasource.metricFindQuery(query)
+    .then(this.transformToSegments(addTemplateVars))
+    .then(results => {
+      if (segment.type === 'key') {
+        results.splice(0, 0, angular.copy(this.removeTagFilterSegment));
+      }
+      return results;
+    })
+    .catch(this.handleQueryError.bind(this));
+  }
+
+  getFieldSegments() {
+    var fieldsQuery = this.queryBuilder.buildExploreQuery('FIELDS');
+    return this.datasource.metricFindQuery(fieldsQuery)
+    .then(this.transformToSegments(false))
+    .catch(this.handleQueryError);
+  }
+
+  setFill(fill) {
+    this.target.fill = fill;
+    this.panelCtrl.refresh();
+  }
+
+  tagSegmentUpdated(segment, index) {
+    this.tagSegments[index] = segment;
+
+    // handle remove tag condition
+    if (segment.value === this.removeTagFilterSegment.value) {
+      this.tagSegments.splice(index, 3);
+      if (this.tagSegments.length === 0) {
+        this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
+      } else if (this.tagSegments.length > 2) {
+        this.tagSegments.splice(Math.max(index-1, 0), 1);
+        if (this.tagSegments[this.tagSegments.length-1].type !== 'plus-button') {
+          this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
+        }
+      }
+    } else {
+      if (segment.type === 'plus-button') {
+        if (index > 2) {
+          this.tagSegments.splice(index, 0, this.uiSegmentSrv.newCondition('AND'));
+        }
+        this.tagSegments.push(this.uiSegmentSrv.newOperator('='));
+        this.tagSegments.push(this.uiSegmentSrv.newFake('select tag value', 'value', 'query-segment-value'));
+        segment.type = 'key';
+        segment.cssClass = 'query-segment-key';
+      }
+
+      if ((index+1) === this.tagSegments.length) {
+        this.tagSegments.push(this.uiSegmentSrv.newPlusButton());
+      }
+    }
+
+    this.rebuildTargetTagConditions();
+  }
+
+  rebuildTargetTagConditions() {
+    var tags = [];
+    var tagIndex = 0;
+    var tagOperator = "";
+
+    _.each(this.tagSegments, (segment2, index) => {
+      if (segment2.type === 'key') {
+        if (tags.length === 0) {
+          tags.push({});
+        }
+        tags[tagIndex].key = segment2.value;
+      } else if (segment2.type === 'value') {
+        tagOperator = this.getTagValueOperator(segment2.value, tags[tagIndex].operator);
+        if (tagOperator) {
+          this.tagSegments[index-1] = this.uiSegmentSrv.newOperator(tagOperator);
+          tags[tagIndex].operator = tagOperator;
+        }
+        tags[tagIndex].value = segment2.value;
+      } else if (segment2.type === 'condition') {
+        tags.push({ condition: segment2.value });
+        tagIndex += 1;
+      } else if (segment2.type === 'operator') {
+        tags[tagIndex].operator = segment2.value;
+      }
+    });
+
+    this.target.tags = tags;
+    this.panelCtrl.refresh();
+  }
+
+  getTagValueOperator(tagValue, tagOperator) {
+    if (tagOperator !== '=~' && tagOperator !== '!~' && /^\/.*\/$/.test(tagValue)) {
+      return '=~';
+    } else if ((tagOperator === '=~' || tagOperator === '!~') && /^(?!\/.*\/$)/.test(tagValue)) {
+      return '=';
+    }
+  }
+}
+

+ 62 - 76
public/app/plugins/datasource/influxdb/specs/query_ctrl_specs.ts

@@ -2,6 +2,7 @@ import '../query_ctrl';
 import 'app/core/services/segment_srv';
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import helpers from 'test/specs/helpers';
+import {InfluxQueryCtrl} from '../query_ctrl';
 
 describe('InfluxDBQueryCtrl', function() {
   var ctx = new helpers.ControllerTestContext();
@@ -14,179 +15,164 @@ describe('InfluxDBQueryCtrl', function() {
   beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
     ctx.$q = $q;
     ctx.scope = $rootScope.$new();
-    ctx.scope.ctrl = {panel: ctx.panel};
-    ctx.scope.datasource = ctx.datasource;
-    ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
-    ctx.panelCtrl = ctx.scope.ctrl;
-    ctx.controller = $controller('InfluxQueryCtrl', {$scope: ctx.scope});
-  }));
-
-  beforeEach(function() {
-    ctx.scope.target = {};
+    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.target = {target: {}};
+    ctx.ctrl = $controller(InfluxQueryCtrl, {$scope: ctx.scope}, {
+      panelCtrl: ctx.panelCtrl,
+      target: ctx.target,
+      datasource: ctx.datasource
     });
+  }));
 
+  describe('init', function() {
     it('should init tagSegments', function() {
-      expect(ctx.scope.tagSegments.length).to.be(1);
+      expect(ctx.ctrl.tagSegments.length).to.be(1);
     });
 
     it('should init measurementSegment', function() {
-      expect(ctx.scope.measurementSegment.value).to.be('select measurement');
+      expect(ctx.ctrl.measurementSegment.value).to.be('select measurement');
     });
   });
 
   describe('when first tag segment is updated', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
     });
 
     it('should update tag key', function() {
-      expect(ctx.scope.target.tags[0].key).to.be('asd');
-      expect(ctx.scope.tagSegments[0].type).to.be('key');
+      expect(ctx.ctrl.target.tags[0].key).to.be('asd');
+      expect(ctx.ctrl.tagSegments[0].type).to.be('key');
     });
 
     it('should add tagSegments', function() {
-      expect(ctx.scope.tagSegments.length).to.be(3);
+      expect(ctx.ctrl.tagSegments.length).to.be(3);
     });
   });
 
   describe('when last tag value segment is updated', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
-      ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
     });
 
     it('should update tag value', function() {
-      expect(ctx.scope.target.tags[0].value).to.be('server1');
+      expect(ctx.ctrl.target.tags[0].value).to.be('server1');
     });
 
     it('should set tag operator', function() {
-      expect(ctx.scope.target.tags[0].operator).to.be('=');
+      expect(ctx.ctrl.target.tags[0].operator).to.be('=');
     });
 
     it('should add plus button for another filter', function() {
-      expect(ctx.scope.tagSegments[3].fake).to.be(true);
+      expect(ctx.ctrl.tagSegments[3].fake).to.be(true);
     });
   });
 
   describe('when last tag value segment is updated to regex', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
-      ctx.scope.tagSegmentUpdated({value: '/server.*/', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button'}, 0);
+      ctx.ctrl.tagSegmentUpdated({value: '/server.*/', type: 'value'}, 2);
     });
 
     it('should update operator', function() {
-      expect(ctx.scope.tagSegments[1].value).to.be('=~');
-      expect(ctx.scope.target.tags[0].operator).to.be('=~');
+      expect(ctx.ctrl.tagSegments[1].value).to.be('=~');
+      expect(ctx.ctrl.target.tags[0].operator).to.be('=~');
     });
   });
 
   describe('when second tag key is added', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
-      ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
-      ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
     });
 
     it('should update tag key', function() {
-      expect(ctx.scope.target.tags[1].key).to.be('key2');
+      expect(ctx.ctrl.target.tags[1].key).to.be('key2');
     });
 
     it('should add AND segment', function() {
-      expect(ctx.scope.tagSegments[3].value).to.be('AND');
+      expect(ctx.ctrl.tagSegments[3].value).to.be('AND');
     });
   });
 
   describe('when condition is changed', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
-      ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
-      ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
-      ctx.scope.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+      ctx.ctrl.tagSegmentUpdated({value: 'OR', type: 'condition'}, 3);
     });
 
     it('should update tag condition', function() {
-      expect(ctx.scope.target.tags[1].condition).to.be('OR');
+      expect(ctx.ctrl.target.tags[1].condition).to.be('OR');
     });
 
     it('should update AND segment', function() {
-      expect(ctx.scope.tagSegments[3].value).to.be('OR');
-      expect(ctx.scope.tagSegments.length).to.be(7);
+      expect(ctx.ctrl.tagSegments[3].value).to.be('OR');
+      expect(ctx.ctrl.tagSegments.length).to.be(7);
     });
   });
 
   describe('when deleting first tag filter after value is selected', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
-      ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
-      ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 0);
     });
 
     it('should remove tags', function() {
-      expect(ctx.scope.target.tags.length).to.be(0);
+      expect(ctx.ctrl.target.tags.length).to.be(0);
     });
 
     it('should remove all segment after 2 and replace with plus button', function() {
-      expect(ctx.scope.tagSegments.length).to.be(1);
-      expect(ctx.scope.tagSegments[0].type).to.be('plus-button');
+      expect(ctx.ctrl.tagSegments.length).to.be(1);
+      expect(ctx.ctrl.tagSegments[0].type).to.be('plus-button');
     });
   });
 
   describe('when deleting second tag value before second tag value is complete', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
-      ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
-      ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
-      ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 4);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+      ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 4);
     });
 
     it('should remove all segment after 2 and replace with plus button', function() {
-      expect(ctx.scope.tagSegments.length).to.be(4);
-      expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
+      expect(ctx.ctrl.tagSegments.length).to.be(4);
+      expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
     });
   });
 
   describe('when deleting second tag value before second tag value is complete', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
-      ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
-      ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
-      ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 4);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+      ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 4);
     });
 
     it('should remove all segment after 2 and replace with plus button', function() {
-      expect(ctx.scope.tagSegments.length).to.be(4);
-      expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
+      expect(ctx.ctrl.tagSegments.length).to.be(4);
+      expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
     });
   });
 
   describe('when deleting second tag value after second tag filter is complete', function() {
     beforeEach(function() {
-      ctx.scope.init();
-      ctx.scope.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
-      ctx.scope.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
-      ctx.scope.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
-      ctx.scope.tagSegmentUpdated({value: 'value', type: 'value'}, 6);
-      ctx.scope.tagSegmentUpdated(ctx.scope.removeTagFilterSegment, 4);
+      ctx.ctrl.tagSegmentUpdated({value: 'asd', type: 'plus-button' }, 0);
+      ctx.ctrl.tagSegmentUpdated({value: 'server1', type: 'value'}, 2);
+      ctx.ctrl.tagSegmentUpdated({value: 'key2', type: 'plus-button'}, 3);
+      ctx.ctrl.tagSegmentUpdated({value: 'value', type: 'value'}, 6);
+      ctx.ctrl.tagSegmentUpdated(ctx.ctrl.removeTagFilterSegment, 4);
     });
 
     it('should remove all segment after 2 and replace with plus button', function() {
-      expect(ctx.scope.tagSegments.length).to.be(4);
-      expect(ctx.scope.tagSegments[3].type).to.be('plus-button');
+      expect(ctx.ctrl.tagSegments.length).to.be(4);
+      expect(ctx.ctrl.tagSegments[3].type).to.be('plus-button');
     });
   });
-
 });